Overload control for scaling WeChat microservices

Overload control for scaling WeChat microservices Zhou et al., SoCC’18

There are two reasons to love this paper. First off, we get some insights into the backend that powers WeChat; and secondly the authors share the design of the battle hardened overload control system DAGOR that has been in production at WeChat for five years. This system has been specifically designed to take into account the peculiarities of microservice architectures. If you’re looking to put a strategy in place for your own microservices, you could do a lot worse than start here.

WeChat

The WeChat backend at this point consists of over 3000 mobile services, including instant messaging, social networking, mobile payment, and third-party authorization. The platform sees between 10^{10} - 10^{11} external requests per day. Each such request can triggers many more internal microservice requests, such that the WeChat backend as a whole needs to handle hundreds of millions of requests per second.

WeChat’s microservice system accommodates more than 3000 services running on over 20,000 machines in the WeChat business system, and these numbers keep increasing as WeChat is becoming immensely popular… As WeChat is ever actively evolving, its microservice system has been undergoing fast iteration of service updates. For instance, from March to May in 2018, WeChat’s microservice system experienced almost a thousand changes per day on average.

WeChat classify their microservices as “Entry leap” services (front-end services receiving external requests), “Shared leap” services (middle-tier orchestration services), and “Basic services” (services that don’t fan out to any other services, and thus act as sinks for requests).

On a typical day, peak request rate is about 3x the daily average. At certain times of year (e.g. around the Chinese Lunar New Year) peak workload can rise up to 10x the daily average.

Challenges of overload control for large-scale microservice-based platforms

Overload control… is essential for large-scale online applications that need to enforce 24×7 service availability despite any unpredictable load surge.

Traditional overload control mechanisms were designed for a world with a small number of service components, a relatively narrow ‘front-door,’ and trivial dependencies.

… modern online services are becoming increasingly complex in their architecture and dependencies, far beyond what traditional overload control was designed for.

  • With no single entry point for service requests sent to the WeChat backend, the conventional approach of centralized load monitoring at a global entry point (gateway) is not applicable.
  • The service invocation graph for a particular request may depend on request-specific data and service parameters, even for requests of the same type. So when a particular service becomes overload it is very difficult to determine what types of requests should be dropped to mitigate the situation.
  • Excessive request aborts (especially when deeper in the call graph or later in the request processing) waste computational resources and affect user experience due to high latency.
  • Since the service DAG is extremely complex and continuously evolving, the maintenance cost and system overhead for effective cross-service coordination is too high.

Since one service may make multiple requests to a service it depends on, and may also make requests to multiple backend services, we have to take extra care with overload controls. The authors coin the term subsequent overload for the cases where more than one overloaded service is invoked, or a single overloaded service is invoked multiple times.

Subsequent overload raises challenges for effective overload control. Intuitively, performing load shedding at random when a service becomes overloaded can sustain the system with a saturated throughput. However, subsequent overload may greatly degrade system throughput beyond that intended…

Consider a simple scenario where service A invokes service B twice. If B starts rejecting half of all incoming requests, A’s probability of success drops to 0.25.

DAGOR overview

WeChat’s overload control system is called DAGOR. It aims to provide overload control to all services and thus is designed to be service agnostic. Overload control runs at the granularity of an individual machine, since centralised global coordination is too expensive. However, it does incorporate a lightweight collaborative inter-machine protocol which is needed to handle subsequent overload situations. Finally, DAGOR should sustain the best-effort success rate of a service when load shedding becomes inevitable due to overload. Computational resources (e.g. CPU, I/O) spent on failed service tasks should be minimised.

We have two basic tasks to address: detecting an overload situation, and deciding what to do about it once detected.

Overload detection

For overload detection, DAGOR uses the average waiting time of requests in the pending queue (i.e., queuing time). Queuing time has the advantage of negating the impact of delays lower down in the call-graph (compared to e.g. request processing time). Request processing time can increase even when the local server itself is not overloaded. DAGOR uses window-based monitoring, where a window is one second or 2000 requests, whichever comes first. WeChat clearly run a tight ship:

For overload detection, given the default timeout of each service task being 500ms in WeChat, the threshold of the average request queuing time to indicate server overload is set to 20ms. Such empirical configurations have been applied in the WeChat business system for more than five years with its effectiveness proven by the system robustness with respect to WeChat business activities.

Admission control

Once overload is detected, we have to decide what to do about it. Or to put things more bluntly, which requests we’re going to drop. The first observation is that not all requests are equal:

The operation log of WeChat shows that when WeChat Pay and Instant Messaging experience a similar period of service unavailability, user complaints against the WeChat Pay service are 100x those against the Instant Messaging service.

To deal with this in a service agnostic way, every request is assigned a business priority when it first enters the system. This priority flows with all downstream requests. Business priority for a user request is determined by the type of action requested. Although there are hundreds of entry points, only a few tens have explicit priority, all the others having a default (lower) priority. The priorities are maintained in a replicated hashtable.

When overload control is set to business priority level n, all requests from levels n+1 will be dropped. That’s great for mixed workloads, but suppose we have a flood of Payment requests, all at the same priority (e.g. p). The system will become overloaded, and hence move the overload threshold to p-1, when it will become lightly loaded again. Once light load is detected, the overload threshold is incremented to p again, and once more we are in overload. To stop this flip-flipping when overloaded with requests at the same priority level, we need a level of granularity finer than business priority.

WeChat has a neat solution to this. It adds a second layer of admission control based on user-id.

User priority is dynamically generated by the entry service through a hash function that takes the user ID as an argument. Each entry service changes its hash function every hour. As a consequence, requests from the same user are likely to be assigned to the same user priority within one hour, but different user priorities across hours.

This provides fairness while also giving an individual user a consistent experience across a relatively long period of time. It also helps with the subsequent overload problem since requests from a user assigned high priority are more likely to be honoured all the way through the call graph.

Combining business priority and user priority gives a compound admission level with 128 levels of user priority per business priority level.

With each admission level of business priority having 128 levels of user priority, the resulting number of compound admission levels is in the tens of thousands. Adjustment of the compound admission level is at the granule of user priority.

There’s a nice sidebar on why using session ID instead of user ID doesn’t work: you end up training users to log out and then log back in again when they’re experiencing poor service, and now you have a login storm on top of your original overload problem!

DAGOR maintains a histogram of requests at each server to track the approximate distribution of requests over admission priorities. When overload is detected in a window period, DAGOR moves to the first bucket that will decrease expected load by 5%. With no overload, it moves to the first bucket that will increase expected load by 1%.

A server piggy-backs its current admission level on each response message sent to upstream servers. In this way an upstream server learns the current admission control setting of a downstream service, and can perform local admission control on the request before even sending it.

End-to-end therefore, the DAGOR overload control system looks like this:

Experiments

The best testimony to the design of DAGOR is that it’s been working well in production at WeChat for five years. That doesn’t provide the requisite graphs for an academic paper though, so we also get a set of simulation experiments. The following chart highlights the benefits of overload control based on queuing time rather than response time. The benefits are most pronounced in situations of subsequent overload (chart (b)).

Compared to CoDel and SEDA, DAGOR exhibits a 50% higher request success rate with subsequent overloading when making one subsequent call. The benefits are greater the higher the number of subsequent requests:

Finally, in terms of fairness CoDel can be seen to favour services with smaller fan-out to overloaded services when under stress, whereas DAGOR manifests roughly the same success rate across a variety of requests.

Three lessons for your own systems

Even if you don’t use DAGOR exactly as described, the authors conclude with three valuable lessons to take into consideration:

  • Overload control in a large-scale microservice architecture must be decentralized and autonomous in each service
  • Overload control should take into account a variety of feedback mechanisms (e.g. DAGOR’s collaborative admission control) rather than relying solely on open-loop heuristics
  • Overload control design should be informed by profiling the processing behaviour of your actual workloads.

中文版链接: https://www.tuicool.com/articles/aYJjMvN

from:https://blog.acolyer.org/2018/11/16/overload-control-for-scaling-wechat-microservices/

基于SpringCloud的微服务架构演变史?

导读
一段时期以来 “微服务架构 ”一直是一个热门词汇,各种技术类公众号或架构分享会议上,关于微服务架构的讨论和主题也都非常多。对于大部分初创互联网公司来说,早期的单体应用结构才是最合适的选择,只有当业务进入快速发展期,在系统压力、业务复杂度以及人员扩展速度都快速上升的情况下,如何快速、稳妥有序的将整个互联网软件系统升级成微服务架构,以满足业务发展需要及技术组织的重新塑造,才是进行微服务架构的最主要的动力,否则空谈微服务架构是没有意义的。

而一旦决定将整个应用体系按照微服务架构体系进行升级,就需要有组织有计划的进行业务系统、基础架构、运维体系等多个方面的升级配套。而另一个比较尴尬的现实是,一般业务发展进入到需要进行微服务架构层面的时候,业务发展往往又都是非常迅猛的,这种业务快速发展和增长的压力往往又会给整个技术团队带来非常大的挑战,因为此时你需要取舍,是简单方案快速支撑呢?还是选择适当长远一点的方案?当然这种情况大部分是技术细节方面的问题,掌控的“度”大部分情况是掌握在具体的工程师手中。

而如何整体上确保应用体系及组织结构向微服务时代快速、有序的跨越,是一件十分考验团队能力以及架构管理水平的事。能做到80分已然算优秀了,因为这是有其客观规律的!

作者自身亲历了一个快速发展的互联网公司从单体应用~以SpringCloud为技术栈的微服务架构体系的全过程。本文将主要从技术角度与大家探讨如何利用SpringCloud进行微服务架构拆分,以及在这个过程中一点自己的思考。水平有限,不足之处还请包涵!
系统架构演变概述


在公司业务初创时期,面对的主要问题是如何将一个想法变成实际的软件实现,在这个时候整个软件系统的架构并没有搞得那么复杂,为了快速迭代,整个软件系统就是由“App+后台服务”组成,而后台服务也只是从工程角度将应用进行Jar包的拆分。此时软件系统架构如下:

而此时整个软件系统的功能也比较简单,只有基本的用户、订单、支付等功能,并且由于业务流程没有那么复杂,这些功能基本耦合在一起。而随着App的受欢迎程度(作者所在的公司正好处于互联网热点),所以App下载量在2017年迅猛增长,在线注册人数此时也是蹭蹭往上涨。

随着流量的迅猛增长,此时整个后台服务的压力变得非常大,为了抗住压力只能不断的加机器,平行扩展后台服务节点。此时的部署架构如下:

通过这种方式,整个软件系统抗住了一波压力,然而系统往往还是会偶尔出点事故,特别是因为api中的某个接口性能问题导致整个服务不可用,因为这些接口都在一个JVM进程中,虽然此时部署了多个节点,但因为底层数据库、缓存系统都是一套,所以还是会出现一挂全挂的情况。

另一方面,随着业务的快速发展,以往相对简单的功能变得复杂起来,这些功能除了有用户看得见的、也会包括很多用户看不见的,就好像百度搜索,用户能看见的可能只是一个搜索框,但是实际上后台对应的服务可能是成百上千,如有些增长策略相关的功能:红包、分享拉新等。还有些如广告推荐相关的变现功能等。

此外,流量/业务的增长也意味着团队人数的迅速增长,如果此时大家开发各自的业务功能还是用一套服务代码,很难想象百十来号人,在同一个工程在叠加功能会是一个什么样的场景。所以如何划分业务边界、合理的进行团队配置也是一件十分迫切的事情了!

为了解决上述问题,适应业务、团队发展,架构团队决定进行微服务拆分。而要实施微服务架构,除了需要合理的划分业务模块边界外,也需要一整套完整的技术解决方案。

在技术方案的选择上,服务拆分治理的框架也是有很多,早期的有如WebService,近期的则有各种Rpc框架(如Dubbo、Thirft、Grpc)。而Spring Cloud则是基于Springboot提供的一整套微服务解决方案,因为技术栈比较新,并且各类组件的支撑也非常全面,所以Spring Cloud就成为了首选。

经过一系列的重构+扩展,整个系统架构最终形成了以app为中心的一套微服务软件系统,结构如下:

到这里,整个软件系统就基于SpringCloud初步完成了微服务体系的拆分。支付、订单、用户、广告等核心功能抽离成独立的微服务,与此同时各自微服务对应的数据库也按照服务边界进行了拆分。

在完成服务的拆分以后,原来功能逻辑之间的代码调用关系,转换成了服务间网络的调用关系,而各个微服务需要根据各自所承载的功能提供相应的服务,此时服务如何被其他服务发现并调用,就成了整个微服务体系中比较关键的部分,使用过Dubbo框架的同学知道,在Dubbo中服务的注册&发现是依赖于Zookeeper实现的,而在SpringCloud中我们是通过Consul来实现。另外在基于SpringCloud的架构体系中,提供了配置中心(ConfigServer)来帮助各个微服务管理配置文件,而原本的api服务,随着各个功能的抽离,逐步演变成前置网关服务了。

聊到这里,基于SpringCloud我们进行了微服务拆分,而在这个体系结构中,分别提到了Consul、ConfigServer、网关服务这几个关键组件,那么这几个关键组件具体是如何支撑这个庞大的服务体系的呢?

SpringCloud关键组件

Consul

Consul是一个开源的,使用go语言开发的注册中心服务。它里面内置了服务发现与注册框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心等多个方案。在SpringCloud框架中还可以选择Eurke作为注册中心,这里之所以选择Consul主要原因在于Consul对异构的服务的支持,如:grpc服务。

事实上,在后续的系统架构演进中,在某些服务模块进一步向子系统化拆分的过程中,就采用了grpc作为子系统服务间的调用方式。例如,支付模块的继续扩张,对支付服务本身又进行了微服务架构的拆分,此时支付微服务内部就采用了grpc的方式进行调用,而服务注册与发现本身则还是依赖于同一套Consul集群。

此时的系统架构演进如下:

原有微服务架构中的模块服务在规模达到一定程度或复杂性达到一定程度后,都会朝着独立的体系发展,从而将整个微服务的调用链路变的非常长,而从Consul的角度看,所有的服务又都是扁平的。

随着微服务规模的越来越大,Consul作为整个体系的核心服务组件,在整个体系中处于关键的位置,一旦Consul挂掉,所有的服务都将停止服务。那么Consul到底是什么样服务?其容灾机制又该如何设计呢?

要保证Consul服务的高可用,在生产环境Consul应该是一个集群(关于Consul集群的安装与配置可以参考网络资料),这是毫无疑问的。而在Consul集群中,存在两种角色:Server、Client,这两种角色与Consul集群上运行的应用服务并没有什么关系,只是基于Consul层面的一种角色划分。实际上,维持整个Consul集群状态信息的还是Server节点,与Dubbo中使用Zookeeper实现注册中心一样,Consul集群中的各个Server节点也需要通过选举的方式(使用GOSSIP协议、Raft一致性算法,这里不做详细展开,在后面的文章中可以和大家单独讨论)来选举整个集群中的Leader节点来负责处理所有查询和事务,并向其他节点同步状态信息。

Client角色则是相对无状态的,只是简单的代理转发RPC请求到Server节点,之所以存在Client节点主要是分担Server节点的压力,作一层缓冲而已,这主要是因为Server节点的数量不宜过多,因为Server节点越多也就意味着达成共识的过程越慢,节点间同步的代价也就越高。对于Server节点,一般建议3-5台,而Client节点则没有数量的限制,可以根据实际情况部署数千或数万台。事实上,这也只是一种策略,在现实的生产环境中,大部分应用只需要设置3~5台Server节点就够了,作者所在的公司一套生产集群中的Consul集群的节点配置就是5个Server节点,并没有额外再设置Client节点。

另外,在Consul集群中还有一个概念是Agent,事实上每个Server或Client都是一个consul agent,它是运行在Consul集群中每个成员上的一个守护进程,主要的作用是运行DNS或HTTP接口,并负责运行时检查和保持服务信息同步。我们在启动Consul集群的节点(Server或Client)时,都是通过consul agent的方式启动的。例如:

consul agent -server -bootstrap -syslog \
-ui \
-data-dir=/opt/consul/data \
-dns-port=53
-recursor=10.211.55.3
-config-dir=/opt/consul/conf
 \
-pid-file=/opt/consul/run/consul.pid \
-client=10.211.55.4 \
-bind=10.211.55.4 \
-node=consul-server01 \
-disable-host-node-id &

以实际的生产环境为例,Consul集群的部署结构示意图如下:

实际生产案例中并没有设置Client节点,而是通过5个Consul Server节点组成的集群,来服务整套生产集群的应用注册&发现。这里有细节需要了解下,实际上5个Consul Server节点的IP地址是不一样的,具体的服务在连接Consul集群进行服务注册与查询时应该连接Leader节点的IP,而问题是,如果Leader节点挂掉了,相应的应用服务节点,此时如何连接通过Raft选举产生的新Leader节点呢?难道手工切换IP不成?

显然手工切换IP的方式并不靠谱,而在生产实践中,Consul集群的各个节点实际上是在Consul Agent上运行DNS(如启动参数中红色字体部分),应用服务在连接Consul集群时的IP地址为DNS的IP,DNS会将地址解析映射到Leader节点对应的IP上,如果Leader节点挂掉,选举产生的新Leader节点会将自己的IP通知DNS服务,DNS更新映射关系,这一过程对各应用服务则是透明的。

通过以上分析,Consul是通过集群设计、Raft选举算法,Gossip协议等机制来确保Consul服务的稳定与高可用的。如果需要更高的容灾级别,也可以通过设计双数据中心的方式,来异地搭建两个Consul数据中心,组成一个异地灾备Consul服务集群,只是这样成本会更高,这就看具体是否真的需要了。

ConfigServer(配置中心)

配置中心是对微服务应用配置进行管理的服务,例如数据库的配置、某些外部接口地址的配置等等。在SpringCloud中ConfigServer是独立的服务组件,它与Consul一样也是整个微服务体系中比较关键的一个组件,所有的微服务应用都需要通过调用其服务,从而获取应用所需的配置信息。

随着微服务应用规模的扩大,整个ConfigServer节点的访问压力也会逐步增加,与此同时,各个微服务的各类配置也会越来越多,如何管理好这些配置文件以及它们的更新策略(确保不因生产配置随意改动而造成线上故障风险),以及搭建高可用的ConfigServer集群,也是确保微服务体系稳定很重要的一个方面。

在生产实践中,因为像Consul、ConfigServer这样的关键组件,需要搭建独立的集群,并且部署在物理机而不是容器里。在上一节介绍Consul的时候,我们是独立搭建了5个Consul Server节点。而ConfigServer因为主要是http配置文件访问服务,不涉及节点选举、一致性同步这样的操作,所以还是按照传统的方式搭建高可用配置中心。具体结构示意图如下:

我们可以单独通过git来管理应用配置文件,正常来说由ConfigSeever直接通过网络拉取git仓库的配置供服务获取就可以了,这样只要git仓库配置更新,配置中心就能立刻感知到。但是这样做的不稳定之处,就在于git本身是内网开发用的代码管理工具,如果让线上实时服务直接读取,很容易将git仓库拉挂了,所以,我们在实际的运维过程中,是通过git进行配置文件的版本控制,区分线上分支/master与功能开发分支/feature,并且在完成mr后还需要手工(通过发布平台触发)同步一遍配置,过程是将新的master分支的配置同步一份到各个configserver节点所在主机的本地路径,这样configserver服务节点就可以通过其本地目录获取配置文件,而不用多次调用网络获取配置文件了。

而另一方面,随着微服务越来越多,git仓库中的配置文件数量也会越来越多。为了便于配置的管理,我们需要按照一定的组织方式来组织不同应用类型的配置。在早期所有的应用因为没有分类,所以导致上百个微服务的配置文件放在一个仓库目录,这样一来导致配置文件管理成本增加,另外一方面也会影响ConfigServer的性能,因为某个微服务用不到的配置也会被ConfigServer加载。

所以后期的实践是,按照配置的层次关系进行组织,将公司全局的项目配置抽象到顶层,由ConfigServer默认加载,而其他所有的微服务则按照应用类型进行分组(通过git项目空间的方式分组),相同的应用放在一个组,然后这个组下单独设立一个名为config的git仓库来存放这个组下相关微服务的配置文件。层次结构如下:

这样应用加载配置的优先级就是“本地配置->common配置->组公共配置->项目配置”这样的顺序。例如某服务A,在项目工程的默认配置文件(“bootstrap.yml/application.yml”)中配置了参数A,同时也在本地项目配置“application-production.yml”配置了参数B,而与此同时,ConfigServer中的common仓库下的配置文件“application.yml/application-production.yml”又分别存在参数C、参数D,同时有个组叫“pay”,其下的默认配置文件“application.yml/application-production.yml”存在参数E、参数F,具体项目pay-api又存在配置文件“pay-api-production.yml”其覆盖了common仓库中参数C、参数D的值。那么此时如果该应用以“spring.profiles.active=production”的方式启动,那么其能获取到的配置参数(通过链接访问:http://{spring.cloud.config.uri}/pay-api-production.yml)就是A、B、C、D、E、F,其中C、D的参数值为pay-api-production.yml中最后覆盖的值。

而对于ConfigServer服务本身来说,需要按照这样的组织方式进行配置类型匹配,例如上述的例子中,假设还存在finance的配置仓库,而pay组下的服务访问配置中心的时候,是不需要finance空间下的配置文件的,所以ConfigServer可以不用加载。这里就需要在ConfigServer服务配置中进行一些配置。具体如下:

spring:
  application:
    name: @project.artifactId@
    version: @project.version@
    build: @buildNumber@
    branch: @scmBranch@
  cloud:
    inetutils:
      ignoredInterfaces:
        - docker0
    config:
      server:
        health.enabled: false
        git:
          uri: /opt/repos/config
          searchPaths: 'common,{application}'
          cloneOnStart: true
          repos:
            pay:
                pattern: pay-*
                cloneOnStart: true
                uri: /opt/repos/example/config
                searchPaths: 'common,{application}'
            finance:
                pattern: finance-*
                cloneOnStart: true
                uri: /opt/repos/finance/config
                searchPaths: 'common,{application}'

通过在ConfigServer服务本身的application.yml本地配置中,设置其配置搜索方式,来实现这样的目的。

网关服务&服务熔断&监控

通过上面两小节的内容,我们相对详细地介绍了基于SpringCloud体系中比较关键的两个服务组件。然而在微服务架构体系中,还有很多关键的问题需要解决,例如,应用服务在Consul中部署了多个节点,那么调用方如何实现负载均衡?

关于这个问题,在传统的架构方案中是通过Nginx实现的,但是在前面介绍Consul的时候只提到了Consul的服务注册&发现、选举等机制,并没有提到Consul如何在实现服务调用的负载均衡。难道基于SpringCloud的微服务体系中的应用服务都是单节点在提供服务,哪怕即使部署了多个服务节点?事实上,我们在服务消费方通过@EnableFeignClients注解开启调用,通过@FeignClient(“user”)注解进行服务调用时,就已经实现了负载均衡,为什么呢?因为,这个注解默认是会默认开启Robbin代理的,而Robbin是实现客户端负载均衡的一个组件,通过从Consul拉取服务节点信息,从而以轮询的方式转发客户端调用请求至不同的服务端节点来实现负载均衡。而这一切都是在消费端的进程内部通过代码的方式实现的。这种负载方式寄宿于消费端应用服务上,对消费端存在一定的代码侵入性,这是为什么后面会出现Service Mesh(服务网格)概念的原因之一,这里就不展开了,后面有机会再和大家交流。

另一需要解决的关键问题是服务熔断、限流等机制的实现,SpringCloud通过集成Netflix的Hystrix框架来提供这种机制的支持,与负载均衡机制一样也是在消费端实现。由于篇幅的关系,这里也不展开了,在后面的文章中有机会再和大家交流。

此外还有Zuul组件来实现API网关服务,提供路由分发与过滤相关的功能。而其他辅助组件还有诸如Sleuth来实现分布式链路追踪、Bus实现消息总线、Dashboard实现监控仪表盘等。由于SpringCloud的开源社区比较活跃,还有很多新的组件在不断的被集成进来,感兴趣的朋友可以持续关注下!

微服务之运维形态

在微服务体系结构下,随着服务数量大量的增长,线上的部署&维护的工作量会变得非常大,而如果还采用原有的运维模式的话,就能难满足需要了。此时运维团队需要实施Devops策略,开发自动化运维发布平台,打通产品、开发、测试、运维流程,关注研发效能。

另外一方面也需要推进容器化(Docker/Docker Swarm/k8s)策略,这样才能快速对服务节点进行伸缩,这也是微服务体系下的必然要求。

微服务泛滥问题

这里还需要注意一个问题,就是实施微服务架构后,如何在工程上管控微服务的问题。盲目的进行微服务的拆分也不是一件很合理的事情,因为这会导致整个服务调用链路变得深不可测,对问题排查造成难度,也浪费线上资源。

重构问题

在早期单体架构方式向微服务架构的转变过程中,重构是一个非常好的方式,也是确保服务规范化,业务系统应用架构合理化的很重要的手段。但是,一般来说,在快速发展阶段也就意味着团队规模的迅速增长,短时间内如何让新的团队有事可做也是一件非常考验管理水平的事情,因为如果招了很多人,并且他们之间呈现一种过渡的竞争状态的话,就会出现让重构这件事变得有些功利的情况,从而导致重构不彻底、避重就轻,导致表象上看是很高大上的微服务架构,而业务系统实际上比较烂的情况。

另外,重构是在一定阶段后作出的重要决策,不仅仅是重新拆分,也是对业务系统的重新塑造,所以一定要考虑好应用软件的系统结构以及实施它们所需要付出的成本,切不可盲目!

后记

基于SpringCloud的微服务架构体系,通过集成各种开源组件来为整个体系服务支持,但是在负载均衡、熔断、流量控制的方面需要对服务消费端的业务进程进行侵入。所以很多人会认为这不是一件很好的事情,于是出现了Service Mesh(服务网格)的概念,Service Mesh的基本思路就是通过主机独立Proxy进行的部署来解耦业务系统进程,这个Proxy除了负责服务发现和负载均衡(不在需要单独的注册组件,如Consul)外,还负责动态路由、容错限流、监控度量和安全日志等功能。

而在具体的服务组件上目前主要是 Google/IBM 等大厂支持和推进的一个叫做Istio的ServiceMesh 标准化工作组。具体关于Service Mesh的知识,在后面的内容中再和大家交流。以上就是本文的全部内容,由于作者水平有限,还请多多包涵!

from:https://mp.weixin.qq.com/s/NHVJCmVUXcAb_pAXxT8mHA

HTTP 的前世今生

作为互联网通信协议的一员老将,HTTP 协议走到今天已经经历了三次版本的变动,现在最新的版本是 HTTP2.0,相信大家早已耳熟能详。今天就给大家好好介绍一下 HTTP 的前世今生。

HTTP/0.9

HTTP 的最早版本诞生在 1991 年,这个最早版本和现在比起来极其简单,没有 HTTP 头,没有状态码,甚至版本号也没有,后来它的版本号才被定为 0.9 来和其他版本的 HTTP 区分。HTTP/0.9 只支持一种方法—— Get,请求只有一行。

  1. GET /hello.html

响应也是非常简单的,只包含 html 文档本身。

  1. <HTML>
  2. Hello world
  3. </HTML>

当 TCP 建立连接之后,服务器向客户端返回 HTML 格式的字符串。发送完毕后,就关闭 TCP 连接。由于没有状态码和错误代码,如果服务器处理的时候发生错误,只会传回一个特殊的包含问题描述信息的 HTML 文件。这就是最早的 HTTP/0.9 版本。

HTTP/1.0

1996 年,HTTP/1.0 版本发布,大大丰富了 HTTP 的传输内容,除了文字,还可以发送图片、视频等,这为互联网的发展奠定了基础。相比 HTTP/0.9,HTTP/1.0 主要有如下特性:

  •  请求与响应支持 HTTP 头,增加了状态码,响应对象的一开始是一个响应状态行
  •  协议版本信息需要随着请求一起发送,支持 HEAD,POST 方法
  •  支持传输 HTML 文件以外其他类型的内容

一个典型的 HTTP/1.0 的请求像这样:

  1. GET /hello.html HTTP/1.0
  2. User-Agent:NCSA_Mosaic/2.0(Windows3.1)
  3. 200 OK
  4. Date: Tue, 15 Nov 1996 08:12:31 GMT
  5. Server: CERN/3.0 libwww/2.17
  6. Content-Type: text/html
  7. <HTML>
  8. 一个包含图片的页面
  9. <IMGSRCIMGSRC=“/smile.gif”>
  10. </HTML>

HTTP/1.1

在 HTTP/1.0 发布几个月后,HTTP/1.1 就发布了。HTTP/1.1 更多的是作为对 HTTP/1.0 的完善,在 HTTP1.1 中,主要具有如下改进:

  •  可以复用连接
  •  增加 pipeline:HTTP 管线化是将多个 HTTP 请求整批提交的技术,而在传送过程中不需先等待服务端的回应。管线化机制须通过永久连接(persistent connection)完成。浏览器将HTTP请求大批提交可大幅缩短页面的加载时间,特别是在传输延迟(lag/latency)较高的情况下。有一点需要注意的是,只有幂等的请求可以使用 pipeline,如 GET,HEAD 方法。
  •  chunked 编码传输:该编码将实体分块传送并逐块标明长度,直到长度为 0 块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据)
  •  引入更多缓存控制机制:如 etag,cache-control
  •  引入内容协商机制,包括语言,编码,类型等,并允许客户端和服务器之间约定以最合适的内容进行交换
  •  请求消息和响应消息都支持 Host 头域:在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。因此,Host 头的引入就很有必要了。
  •   新增了 OPTIONS,PUT, DELETE, TRACE, CONNECT 方法

虽然 HTTP/1.1 已经优化了很多点,作为一个目前使用最广泛的协议版本,已经能够满足很多网络需求,但是随着网页变得越来越复杂,甚至演变成为独立的应用,HTTP/1.1 逐渐暴露出了一些问题:

  •  在传输数据时,每次都要重新建立连接,对移动端特别不友好
  •  传输内容是明文,不够安全
  •  header 内容过大,每次请求 header 变化不大,造成浪费
  •  keep-alive 给服务端带来性能压力

为了解决这些问题,HTTPS 和 SPDY 应运而生。

HTTPS

HTTPS 是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。

HTTPS 协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。

HTTPS 和 HTTP 的区别主要如下:

  •  HTTPS 协议使用 ca 申请证书,由于免费证书较少,需要一定费用。
  •  HTTP 是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
  • HTTP 和 HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。

SPDY

其实 SPDY 并不是新的一种协议,而是在 HTTP 之前做了一层会话层。

在 2010 年到 2015 年,谷歌通过实践一个实验性的 SPDY 协议,证明了一个在客户端和服务器端交换数据的另类方式。其收集了浏览器和服务器端的开发者的焦点问题,明确了响应数量的增加和解决复杂的数据传输。在启动 SPDY 这个项目时预设的目标是:

  •  页面加载时间 (PLT) 减少 50%。
  •  无需网站作者修改任何内容。
  •  将部署复杂性降至最低,无需变更网络基础设施。
  •  与开源社区合作开发这个新协议。
  •  收集真实性能数据,验证这个实验性协议是否有效。

为了达到降低目标,减少页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。

HTTP/2.0

时间来到 2015 年,HTTP/2.0 问世。先来介绍一下 HTTP/2.0 的特点吧:

  •  使用二进制分帧层:在应用层与传输层之间增加一个二进制分帧层,以此达到在不改动 HTTP 的语义,HTTP 方法、状态码、URI 及首部字段的情况下,突破HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层上,HTTP2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。

  •  多路复用:对于 HTTP/1.x,即使开启了长连接,请求的发送也是串行发送的,在带宽足够的情况下,对带宽的利用率不够,HTTP/2.0 采用了多路复用的方式,可以并行发送多个请求,提高对带宽的利用率。

  •  数据流优先级:由于请求可以并发发送了,那么如果出现了浏览器在等待关键的 CSS 或者 JS 文件完成对页面的渲染时,服务器却在专注的发送图片资源的情况怎么办呢?HTTP/2.0 对数据流可以设置优先值,这个优先值决定了客户端和服务端处理不同的流采用不同的优先级策略。
  •  服务端推送:在 HTTP/2.0 中,服务器可以向客户发送请求之外的内容,比如正在请求一个页面时,服务器会把页面相关的 logo,CSS 等文件直接推送到客户端,而不会等到请求来的时候再发送,因为服务器认为客户端会用到这些东西。这相当于在一个 HTML 文档内集合了所有的资源。
  •  头部压缩:使用首部表来跟踪和存储之前发送的键值对,对于相同的内容,不会再每次请求和响应时发送。

可以看到 HTTP/2.0 的新特点和 SPDY 很相似,其实 HTTP/2.0 本来就是基于 SPDY 设计的,可以说是 SPDY 的升级版。

但是 HTTP/2.0 仍有和 SPDY 不同的地方,主要有如下两点:

  •  HTTP2.0 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS。
  •  HTTP2.0 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE。

from:http://developer.51cto.com/art/201811/586932.htm

深度解析JavaScript的this关键字

这篇文章通过简单的术语和一个真实的例子解释了 this 是什么以及为什么说它很有用。

你的 this

我发现,很多教程在解释 JavaScript 的 this 时,通常会假设你拥有 Java、C++ 或 Python 等面向对象编程语言的背景。这篇文章主要面向那些对 this 没有先入之见的人。我将尝试解释什么是 this 以及为什么它很有用。

或许你迟迟不肯深入探究 this,因为它看起来很奇怪,让你心生畏惧。你之所以使用它,有可能仅仅是因为 StackOverflow 说你需要在 React 用它来完成一些事情。

在我们深入了解它的真正含义以及为什么要使用它之前,我们首先需要了解函数式编程和面向对象编程之间的区别。

函数式编程与面向对象编程

你可能知道也可能不知道,JavaScript 具有函数和面向对象的构造,你可以选择关注其中一个或两者兼而有之。

在我的 JavaScript 之旅的早期,我一方面拥抱函数式编程,一方面像避免瘟疫一样排斥面向对象编程。我对面向对象关键字 this 不甚了解。其中的一个原因是我不明白它存在的必要性。在我看来,完全可以不依赖 this 就可以完成所有的事情。

在某种程度上,我的看法是对的。

你可能只关注其中一种范式而从来不去了解另外一种,作为一名 JavaScript 开发者,你的局限性就体现在这里。为了说明函数式编程和面向对象编程之间的差别,我将使用一组 Facebook 好友数据作为示例。

假设你正在构建一个用户登录 Facebook 的 Web 应用,在登录后显示一些 Facebook 好友的数据。你需要访问 Facebook 端点来获取好友的数据,可能包含一些信息,例如 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts。

const data = [
  {
    firstName: 'Bob',
    lastName: 'Ross',
    username: 'bob.ross',    
    numFriends: 125,
    birthday: '2/23/1985',
    lastTenPosts: ['What a nice day', 'I love Kanye West', ...],
  },
  ...
]

你从(臆造的)Facebook API 获得上面的数据。现在,你需要转换它们,让它们符合项目需要的格式。假设你要为每个用户的朋友显示以下内容:

  • 它们的名字,格式为$ {firstName} $ {lastName}
  • 三篇随机的帖子;
  • 从他们生日起到现在的天数。
函数式方法

如果使用函数式方法,就是将整个数组或数组的每个元素传给一个返回所需操作数据的函数:

const fullNames = getFullNames(data)
// ['Ross, Bob', 'Smith, Joanna', ...]

你从原始数据开始(来自 Facebook API),为了将它们转换为对你有用的数据,你将数据传给一个函数,这个函数将输出你可以在应用程序中显示给用户的数据。

你也可以通过类似的方式获取三个随机帖子并计算朋友生日至今的天数。

函数式方法就是指接受原始数据,将数据传给一个或多个函数,并输出对你有用的数据。

面向对象方法

对于那些刚接触编程和学习 JavaScript 的人来说,面向对象方法可能会更难掌握。面向对象是指你将每个朋友转换为对象,对象包含了用于生成你所需内容的一切。

你可以创建包含 fullName 属性的对象,以及 getThreeRandomPosts 和 getDaysUntilBirthday 函数。

function initializeFriend(data) {
  return {
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from data.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use data.birthday to get the num days until birthday
    }
  };
}
const objectFriends = data.map(initializeFriend)
objectFriends[0].getThreeRandomPosts() 
// Gets three of Bob Ross's posts

面向对象方法是为你的数据创建对象,这些对象包含了状态和用于生成对你和你的项目有用的数据的信息。

这与 this 有什么关系?

你可能没有想过会写出类似 initializeFriend 这样的东西,你可能会认为它很有用。你可能还会注意到,它其实并非真正的面向对象。

getThreeRandomPosts 或 getDaysUntilBirthday 方法之所以有用,主要是因为闭包。因为使用了闭包,所以在 initializeFriend 返回之后,它们仍然可以访问 data。

假设你写了另一个方法,叫 greeting。请注意,在 JavaScript 中,方法只是对象的一个属性,这个属性的值是一个函数。我们希望 greeting 可以做这些事情:

function initializeFriend(data) {
  return {
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from data.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use data.birthday to get the num days until birthday
    },
    greeting: function() {
      return `Hello, this is ${fullName}'s data!`
    }
  };
}

这样可以吗?

不行!

新创建对象的所有东西都可以访问 initializeFriend 的变量,但对象本身的属性或方法不行。当然,你可能会问:

难道你不能用 data.firstName 和 data.lastName 来返回 greeting 吗?

当然可以。但如果我们还想在 greeting 中包含朋友生日至今的天数,该怎么办?我们必须以某种方式从 greeting 中调用 getDaysUntilBirthday 方法。

是时候让 this 上场了!

那么,this 是什么

在不同的情况下,this 代表的东西也不一样。默认情况下,this 指向全局对象(在浏览器中,就是 window 对象)。但光知道这点对我们并没有太大帮助,对我来说有用的是 this 的这条规则:

如果 this 被用在一个对象的方法中,并且这个方法在对象的上下文中调用,那么 this 就指向这个对象本身。

你会问:“在对象的上下文中调用……这又是什么意思”?

别担心,稍后我们会解释这个。

因此,如果我们想在 greeting 中调用 getDaysUntilBirthday,可以直接调用 this.getDaysUntilBirthday,因为在这种情况下,this 指向对象本身。

注意:不要在全局作用域或在另一个函数作用域内的常规 ole 函数中使用 this!this 是一个面向对象的构造。因此,它只在对象(或类)的上下文中有意义!

让我们重构 initializeFriend,让它使用 this:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    }
  };
}

现在,在执行完 intializeFriend 后,这个对象的所有东西都限定在对象本身。我们的方法不再依赖于闭包,它们将使用对象本身包含的信息。

这是 this 的一种使用方式,现在回到之前的问题:为什么说 this 因上下文不同而不同?

有时候,你希望 this 可以指向不一样的东西,比如事件处理程序就是一个很好的例子。假设我们想在用户点击链接时打开朋友的 Facebook 页面。我们可能会在对象中添加一个 onClick 方法:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

请注意,我们向对象添加了 username,让 onFriendClick 可以访问它,这样我们就可以在新窗口中打开朋友的 Facebook 页面。现在编写 HTML:

<button id="Bob_Ross">
  <!-- A bunch of info associated with Bob Ross -->
</button> 

然后是 JavaScript:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

在上面的代码中,我们为 Bob Ross 创建了一个对象。我们获得与 Bob Ross 相关的 DOM 元素。现在我们想要调用 onFriendClick 方法来打开 Bob 的 Facebook 页面。应该没问题吧?

不行!

什么地方出了问题?

请注意,我们为 onclick 处理程序选择的函数是 bobRossObj.onFriendClick。看到问题所在了吗?如果我们像这样重写它:

bobRossDOMEl.addEventListener("onclick", function() {
  window.open(`https://facebook.com/${this.username}`)
})

现在你看到问题所在了吗?当我们将 onclick 处理程序指定为 bobRossObj.onFriendClick 时,我们实际上是将 bobRossObj.onFriendClick 的函数作为参数传给了处理程序。它不再“属于”bobRossObj,也就是说 this 不再指向 bobRossObj。这个时候 this 实际上指向的是全局对象,所以 this.username 是 undefined 的。

是时候让 bind 上场了!

显式绑定 this

我们需要做的是将 this 显式绑定到 bobRossObj。我们可以使用 bind 来实现:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj)
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

之前,this 是基于默认规则设置的。通过使用 bind,我们在 bobRossObj.onFriendClick 中将 this 的值显式设置为对象本身,也就是 bobRossObj。

到目前为止,我们已经知道为什么 this 很有用以及为什么有时候需要显式绑定 this。接下来我们要讨论的最后一个主题是箭头函数。

箭头函数

你可能已经注意到,箭头函数像是一个时髦的新事物。人们似乎很喜欢它们,因为它们简洁而优雅。你可能已经知道它们与一般函数略有不同,但不一定非常清楚这些区别究竟是什么。

或许箭头函数的不同之处在于:

在箭头函数内部,无论 this 处于什么位置,它指的都是相同的东西。

让我们用 initializeFriend 示例解释一下。假设我们想在 greeting 中添加一个辅助函数:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      function getLastPost() {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

这样可以吗?如果不行,要怎样修改才行?

这样当然是不行的。因为 getLastPost 不是在对象的上下文中调用的,所以 getLastPost 中的 this 会回退到默认规则,即指向全局对象。

“在对象的上下文中调用”可能是一个比较含糊的概念。要确定一个函数是否是在“对象的上下文中”被调用,最好的办法是看一下函数是如何被调用的,以及是否有对象“附加”在函数上。

让我们来看看执行 bobRossObj.onFriendClick() 时会发生什么:“找到 bobRossObj 对象的 onFriendClick 属性,调用分配给这个属性的函数”。

再让我们来看看执行 getLastPost() 时会发生什么:”调用一个叫作 getLastPost 的函数”。有没有注意到,这里并没有提及任何对象?

现在来测试一下。假设有一个叫作 functionCaller 的函数,它所做的事情就是调用其他函数:

functionCaller(fn) {
  fn()
}

如果我们这样做会怎样:functionCaller(bobRossObj.onFriendClick)?可不可以说 onFriendClick 是“在对象的上下文中”被调用的?this.username 的定义存在吗?

让我们来看一下:“找到 bobRossObj 对象的 onFriendClick 属性。找到这个属性的值(恰好是一个函数),将它传给 functionCaller,并命名为 fn。现在,执行名为 fn 的函数”。请注意,函数在被调用之前已经从 bobRossObj 对象中“分离”,因此不是“在对象 bobRossObj 的上下文中”调用,所以 this.username 是 undefined 的。

让箭头函数来救场:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const getLastPost = () => {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

箭头函数是在 greeting 中声明的。我们知道,当我们在 greeting 中使用 this 时,它指向对象本身。因此,箭头函数中的 this 指向的对象就是我们想要的。

英文原文:

https://medium.freecodecamp.org/a-deep-dive-into-this-in-javascript-why-its-critical-to-writing-good-code-7dca7eb489e7

Free Web Hosting