微服务架构的相关原理和服务交付

2020 年 6 月 18 日

微服务架构的相关原理和服务交付

本文是 2019 年上海运维部指导内部微服务应用发布平台开发和交付的理论性文档。主要内容是向初步接触微服务的同学介绍微服务相关的基本原理。


01 服务的拆分原则


很多时候,在产品和架构设计的时候,都会遇到有人来问,这个功能为什么这样设计,遵循了什么原则等问题。


那么,实际上在产品和架构设计的时候,到底遵循什么原则呢?


其实比较简单,就是遵循在预期的运行场景之下以相对均衡的资源解决业务问题的原则。做到既不过多依赖、也不过度设计。


在写这篇文章之前,我做了一个简单的实验:


实现一个微服务架构下的博客抓取的功能


按照功能内聚进行拆分的原则,我把这个大的系统拆分为 APIServer,Scheduler,Crawler 三个服务:


  • APIServer 作为整个系统的入口,提供Restful的接口用来管理各个参与到业务中的对象;

  • Scheduler 负责根据定义好的抓取周期把抓取任务均衡到每个 Crawler 上面去;

  • Crawler 就作为最终爬取博客索引和博客内容的Agent。


在这个服务拆分的结构中,Crawler 的功能是内聚的,它提供 RPC 的接口供 Scheduler 去调用,它的主要功能就是抓取博客的索引以及根据索引去下载博客的内容。


对于 Scheduler 而言,它主要是去查看系统里面定义的抓取任务,然后通过 RPC 调用 Crawler 的服务。


如果我们抓取任务这个对象必须通过 APIServer 去获取的的话,那么就必须在 APIServer 那边定义接口和实现供 Scheduler 去调用。需要提供:


  • List接口:供Scheduler查询哪些任务需要执行;

  • Update接口:供Scheduler在执行之前或者执行之后更新抓取任务的属性,以决定下次什么时候再去抓取。


这个架构看上去没有什么问题。在业务规模小的时候我们甚至还可以把 APIServer 和 Scheduler 的功能合并在一起,这样可以减少一些 RPC 模版代码和调用损耗。其实就是减少开发和维护的成本。


但是,当我们需要对这些服务实施高可用的时候,就意味着我们不得不考虑每个服务的实际负载和扩容方式。


对于 Crawler 来讲:


这是一个没有状态的服务,它可以水平扩容。水平扩容就是多起一些相同的副本,供 Scheduler 去调用。


而对于 Scheduler 来讲:


它是否可以水平扩容呢?这个取决于这个服务的实现方式:


a. 如果这些任务是存放在消息队列的话,那么 Scheduler 本身是不需要有任何控制重复的逻辑的,直接从消息队列取就好了。任务执行的唯一性由消息队列取的时候保证。


b. 但是如果这个取任务的逻辑是 Scheduler 自身维护的,比如通过取数据库中的任务来实现,为了让 Scheduler 支持水平扩容,必须再为这组 Scheduler 的服务实现一个分布式的锁来支持每个 Scheduler 实例读取到不同的任务去执行。


至于 APIServer 本身,仅仅是用来进行各个参与到业务中的对象的管理,也是可以水平扩容的。


在上面的例子中,我们可以明显看出 APIServer,Scheduler 和 Crawler 三个服务所负责的功能完全不同,也可以看出哪些服务是可以水平扩容的,哪些是需要谨慎考虑如何进行水平扩容的。根本原因就在于这个业务场景要求在保证系统高可用的同时确保数据的一致性。


通过上面的问题的分析,我们基本是可以得出微服务架构的一个重要特点就是: 架构中的各个服务按照单一功能内聚的方式进行拆分 ,这样方便每个独立的功能分配给独立的小组去开发。


比如对于上面的 Crawler 服务来讲,我们就可以让不同的人去开发面向不同网站的爬虫功能。


02 服务注册与发现


服务发现的概念比较简单,但是微服务架构中的服务发现并不简单。


配置文件方案


我们假设存在一个前后端分离的项目,前端项目 FE 通过调用后端服务 BE 的接口来实现业务逻辑交互。在这个过程中,前端的项目可以在配置文件里面写入后端服务的地址来进行通信。


在这种情况下,从宏观层面,对于前端项目来说后端项目的发现是通过配置好的地址来完成的。


实际上,通过在配置文件里面写入一个或者多个目标通信服务的地址是一开始我们进行服务发现的最简单的方案。比如对于 Nginx 的反向代理来说,我们通过 upstream 指定用于定义每个后端的 server 指令就是如此。


这种配置文件的方案存在什么问题呢?


其中最简单的问题就是维护不方便,无法适应动态变化。


假设服务 A 需要调用服务 B 的接口,然后在服务 A 的配置文件里面配置了三个服务 B 的实例地址。在正常的情况下,服务 A 可以通过轮询或者其他带权重的方式来访问服务 B 的任何一个实例。但是假设这个时候服务 B 的实例有一个出现了故障,在这种情况下,我们必须从配置文件里面把这个实例地址移除,然后重启 A 服务,才能够让 A 不再调用已故障的服务 B 的实例。


当然对于 Nginx 来讲,我们可以配置让 Nginx 忽略某些已故障的 upstream server。但是对于一般的业务服务 A 来讲,把 Nginx 的这套逻辑重新实现一遍很明显成本并不低。另外当这个大的业务体系里面有太多像 A 这样的服务时,实现故障侦测并自动忽略的成本就更高了。


除了服务故障之外,服务 B 的实例地址变更也是个大问题。在传统的基于物理机或者虚拟机的情况下,服务 B 的实例地址升级或者重启后可以保持不变。


但是在今天 Kubernetes 等基于容器的分布式编排系统中,服务 B 的实例地址变更是很平常的事情。无论是实例升级还是实例故障重启或者是实例被 Kubernetes 自动调度都会导致服务 B 的实例地址发生变更。所以对于服务 A 来讲,通过配置文件去维护服务 B 的地址简直就是不可能的。


数据库的方案


既然基于配置文件的方案不方便进行维护,那么我们可以基于数据库的方案来支持动态的服务地址修改。


在很多的系统中,这种方式仍然存在着。我们可以通过编程语言调用 SQL 的方式来完整配置的更新。似乎看上去比基于配置文件的方式要快得多。


但是我们上面还提到一个关于维护服务实例之外的问题,即服务的状态侦测。


当有实例故障的时候我们要停止到该实例的调用,然后在该服务实例恢复正常之后,还要能够恢复对该实例的调用。这里面就不可避免地要周期性地轮询这些服务的状态,然后去更新数据库。


这种周期性轮询的时间间隔就和你能容忍的服务失败时间和服务恢复时间有关系。如果你希望能够尽快发现服务失败或恢复正常的服务调用,那么这个轮询间隔就比较短。


所以在这种方案的情况下,你的服务自身仍然需要去实现这个服务侦测的逻辑。本质上和基于配置文件的方式没有区别。


在微服务架构中,由于服务的创建和销毁频率远比传统的服务要高,这会导致各个服务实例的地址发生大量的变化,所以无论是基于配置文件还是基于数据库,这种维护工作量还是无法实现的。而且由于各个服务自身要实现对提供服务的目标实例进行健康侦测,无疑会给业务代码带来大量重复的而又与业务无关的功能。


注册中心方案


既然这种服务发现的功能和服务健康侦测的功能需要每个服务都要具备,那么我们就可以把这组功能独立出来,交给专门的服务去完成,也就是说服务的实例地址有个中心化的管理。


这个服务就是注册中心。


对于上面的两个问题,注册中心通过如下的方法来实现:


I. 首先,每个服务启动完毕之后就向注册中心发送自己的服务名称和实例地址。


II. 然后,注册中心负责侦测每个服务实例的健康状态并决定是否将该实例地址返回给根据服务名称查询实例地址的请求方。


III. 最后,每个服务调用方拿着这个目标服务地址去请求相关的接口,完成业务逻辑的交互。


在上面的方案中,由于注册中心接管了服务的注册和查询,所以所有的业务服务就不再需要维护目标服务的实例地址,也不再需要去侦测它们的健康状况了,一切都交给了注册中心去完成。


本节小结


微服务架构架构的产生,主要是随着大型业务系统的部署和运维的架构的变化而产生的。技术和架构的演进,都是有迹可循的。思考一个架构存在的合理性的关键就是思考它面对的场景。


PS


如果我们稍微发散一下就会发现其实域名解析服务器可以作为最典型的服务注册中心。


我们可以把域名当作服务的名称,把域名解析到的具体地址作为实例地址。通过 DNS 的协议,我们可以利用域名来查询到具体的 IP 地址;这个和我们通过服务名称查询到实例地址的过程是一样的。通过域名我们屏蔽了具体的 IP 地址,为 IP 地址的维护提供了便利。


基本上所有的 HTTP 客户端库在底层都能够自动探测解析到的目标服务器的 IP 连通性,然后决定连接哪个具体的 IP;而当有任何一个 IP 故障时,我们都可以把它从解析里面去除。通过服务端和客户端两边都支持的方式来实现高可用。


而通过服务名称去查询实例地址,就让我们屏蔽了具体的实例地址的维护过程。


所以在微服务架构中,所有的服务之间都是直接通过服务名称去调用的,具体的调用到哪个实例地址完全是由注册中心和你的微服务客户端决定的。


03 流量的负载均衡


首先我们要明白负载均衡是什么,以及实施负载均衡的意义。


负载均衡的含义


负载均衡解决的是将一个客户端的流量以某种符合最大化资源利用率的方式均摊到服务端所提供的所有实例上的问题。在这个问题的场景中,后端服务的实例是通过水平扩展的方式来提供高可用的。


在明白这个问题之后,我们就必须了解到关于负载均衡的一个最基本的问题。这个问题就是资源的规模是由后端实例数量所决定的;而负载的均衡策略则是由客户端决定的。


客户端的角色


在这里,一个服务的具体角色是客户端还是服务端,是由我们观察问题的视角决定的。


换句话说,同一个服务从不同的角度去看,它极可能在一个角度下是客户端,而在另外一个角度下是服务端。


以一个最简单的例子来讲,一个移动端的 APP 通过域名去访问一个后端的服务,而在这个后端服务之前,我们用了一个 Nginx 代理来做负载均衡。如下图所示:



那么问题来了,作为 Nginx 它的具体角色是什么呢?


当我们从移动端 APP 的角度去看,Nginx 就是一个服务端。而从 Nginx 的角度去看后端服务,Nginx 就是一个客户端。


如我们所知道的那样,Nginx 拥有自己的负载均衡的算法,从而实现前端流量到后端实例的负载均衡。所以我们基本上可以了解到是客户端最终决定了去往后端实例的具体流量。所以研究负载均衡算法问题,就是去特别关注在整个系统中,处于客户端角色的对象。


这个时候或许你会觉得移动端 APP 也是客户端,而且是纯粹的客户端,它是不是也决定了负载均衡的目标呢?


答案是肯定的。因为当 Nginx 的域名如果解析到多个 IP 的话,这些 IP 都会参与到移动端 APP 的请求目标地址中。



比如在移动端经常使用的 HTTP 库 OkHttp 的实现中,使用的是一种轮询的策略来决定将请求发送给域名解析出来的哪个 IP 的。


只是这个轮询的策略采用的是主备的方式,即当上一个 IP 失败就去重试下一个 IP,直到找个一个可用的 IP 就用找到的那个。


虽然这实际情况下并没有实现流量的均衡,但是至少实现了高可用。证明了流量的导向是由客户端决定的这个事实。


在实际的情况下,我们可以自定义一个 DNS 的对象传递给 OkHttpClient,在 DNS 的解析层面把 IP 以轮询或者随机的策略返回,这样就可以实现负载的均衡分配了。


微服务的客户端


现在让我们回到微服务领域,看看现在最常用的基于 Spring Boot 的微服务中的一种负载均衡的方案。这个方案是基于注册中心 Eureka 来实现的。Eureka 本身包含两个部分:


  • 服务端Eureka Server,用来提供服务的注册和查询功能;

  • 客户端Eureka Client,用来从服务端根据目标微服务名称获取实例的地址的功能,然后采用轮询的负载均衡策略去调用目标服务。


因为 Eureka Client 的功能是集成到 Spring Boot 的项目中的,所以这种方式本质上和根据域名解析到实际要调用的目标 IP 原理上是一样的。


当然,这种通过 Eureka Client 直连的方式仍然会存在一些问题,所以后期又发展出了新的方案用来实现负载均衡。


但是无论是哪种方案,从目标服务的角度来看,都是由客户端来决定负载的方向的。而新的方案的产生也是因为出现了新的问题,并不是凭空出现的。所以架构设计仍然是有迹可循、紧盯问题的发展,来进行架构的决策。


本节小结


负载均衡的资源是由服务端提供的,但是实现是由客户端来实现的。


04 服务的限流和熔断


资源限制


在我们之前谈到负载均衡的时候,我们了解到日常使用的关于负载均衡策略的两种方式:轮询和主备。而且我们也了解到是由客户端来决定具体的负载均衡策略的。


那么,作为负载均衡的具体载荷的服务端,是不是除了提供水平扩展的资源之外,就万事大吉了呢?


答案当然是否定的。资源这个东西,总是紧缺的或者说在需要它的时候总是会不够的。


当遇到资源不足的问题时,我们如何解决系统的高可用呢?


如果存在这那么一天,所有的后端资源都是充足的,所有的负载通过轮询的均衡策略就可以完美解决问题。那么我们今天所要讨论的限流和熔断就没有意义了。


动态负载


我们知道,人生下来就是不一样的,哪怕同卵双胞胎也是不一样的。所以没有那两台业务服务器在实际运行情况下,状态是一模一样的。


即使你同时买的一批次服务器,预置的软硬件一样,部署的服务也都一模一样,实际运行起来还是会不一样的,因为你不能保证你处理的任务输入是一样的。


例如它们都是图片缩放用途的服务器,输入的图片有不同的格式,有不同的大小,这就会决定它们即使在轮询的均衡负载之下,最终运行的实际状态都是不同的。


业务服务器运行状态的不同主要体现在 CPU 的利用率,并发线程的数量,内存使用情况,网卡负载,磁盘 IO 速度等几个关键要素。


即使在请求数均摊的情况下,这些业务服务器的状态跑着跑着就不一样了。最终导致的结果是有些服务器响应快,而有些比较慢。


但是如果还是采用轮询均衡负载的方式的话,很快有些服务器就会雪上加霜,而另外一些服务器则空闲的很。


从上面的情况我们就会了解到实际生产环境中轮询这种均衡负载的方式并不一定很科学。我们需要结合业务服务器的实际运行情况去动态的调整负载。


假设让服务 A 直接通过注册中心找到服务 B 的实例地址然后直接轮询调用的话,我们必须让 A 能够自动侦测 B 服务的响应情况来动态均衡负载。相当于服务 A 又要去实现一个动态的负载均衡算法。


这个在实际情况下,成本仍然是很高的。


因为你想一个系统不可能只有两个服务,每个服务都要去实现这个算法,还不如像服务注册和发现放到注册中心实现那样,把这个动态均衡算法放到一个独立的服务中去实现。


微服务网关


在计算机领域里面,凡是和功能或业务没有内聚的模块或组件,我们都要解耦。


解耦的一种方式就是使用代理。在下面的架构图中,微服务网关就是充当代理的角色。


在微服务架构中网关的作用和计算机网络里面的网关作用一样,就是所有内部流量的出口或外部流量的入口。既然有了集中的入口,那么很多事情就可以让入口去做了。


在这里微服务网关做的一件事情就是动态负载,具体就分为限流和熔断。



为了能够形象解释下限流和熔断这个概念,我们举个非常实际的例子:


有个银行发生了挤兑的情况。门外是成千上万的人群(流量),门内是十来个办理业务的窗口(后端服务)。而门口站着一个保安(网关)。


对于这个保安来讲,他有两种选择:


a. 一种是直接放开大门,让所有人往里面挤


这种情况下虽然能有一部分的业务需求得到了处理,但是大部分人的请求都被放弃了。


因为后端服务可能被挤垮了。柜台说不定都被砸了。这个在江湖上叫做漏桶算法。桶就那么大,多灌进去又没有从管道流出的流量就溢出了。灌的越快,溢出的越多。


b. 另外一种选择是发号,就像现在银行大堂那样


拿到号的人往里走去办理业务,其他人就堵在门口就好了。


这样来讲,服务还是正常的,只是并行处理的业务数得到了控制,能够缓解后端的压力,不至于直接打垮。这个在江湖上叫做令牌桶算法。


一般来讲,我们应对的高峰流量有两种类型:


  • 一种是预期的高峰流量,比如电商购物节之类的,这种可以通过预先扩充资源来解决问题。

  • 另外一种是突发性高峰流量,就像微博那样突然就有几对明星并行出轨了,这种情况下肯定得限流,不限流没人打得开网页,限流了最多慢一点。


上面我们介绍的是作为微服务网关的限流场景所解决的问题。现在我们来看看什么时候服务发生熔断。


熔断从字面意思很好理解,就是服务断了。


例如后端的窗口工作人员持续工作三天三夜全部趴下了,这个时候外面的流量发令牌也没用,挤进去更没用。身为网关的保安说,“大家散了吧,去别的分行去挤兑吧”。


熔断是为了保护后端的服务,一般来讲不会真的 100%把后端服务打挂才去熔断,而是有个临界值,毕竟已经进来的请求要能处理完会比较好。


熔断的目的是不再接收新的流量,以免造成服务雪崩,再也爬不起来。就像保险丝一样,你不能等到家电全部着火电流非常大的时候再去熔断(不信你把保险丝换成铁丝看看,有人就这么干)。


本节小结


微服务架构本身解决的就是业务规模化之后的问题。


换句话说,如果你的业务规模不大(现在不大,以后也不会大),考虑这些问题都还太早。


微服务的架构中根据团队职责分组,根据服务功能拆分微服务,以及需要服务之间协作而产生的注册中心和服务发现,包括应对大流量的负载均衡,解决资源有限情况下的限流和熔断,都是随着需要解决的问题而应运而生的。所以架构决策有迹可循,紧跟问题的发展,做出应对的决策。


所以说,能够参与解决问题的人成长最快,最幸福啊!


05 微服务应用交付平台


代码即服务


代码即服务(Code-as-a-Service)是微服务架构下独特的服务交付模式。


单体模式发布


在传统的单体应用中,一般系统的部署都是从手动部署开始的。通过编写一些部署脚本,然后到指定的服务器上面去执行来发布新的服务版本,最后重启服务进程完成服务的升级过程。


当服务的负载随着业务规模的扩大时,就需要对部署的服务实例进行扩充。这个时候我们一般都会在多个服务器上面去部署服务的实例,也就是同一个脚本在多个服务器上面执行一遍。


为了减少在这种部署方式下人工的介入,以避免发生一些低级错误,随之就产生了一些例如 Ansible 之类的可以批量执行脚本的工具。甚至有些公司内部自己结合自己的实际情况开发了一些工具。


在传统的部署模式中,一般都会有一台叫做堡垒机或者跳板机的服务器,大家都把要发布的服务的新包推送到这台机器,然后通过这些机器去执行分发的过程。


这个过程严重依赖运维人员的手动操作,而且在多实例部署的情况下,很容易出现版本不一致的情况。印象中,有听说过线上服务升级时各个实例的版本都不一致的情况。所以这确实是实际存在的问题。假设有 100 个实例要更新,谁能保证手动操作之下不出现一些问题呢?


所以半自动化或者全自动化的部署方式及相关的工具和产品就应运而生了。


微服务架构特点


我们现在来思考一下,在微服务架构中业务最主要的特点是什么呢?


根据我们之前提到过的,就是根据功能来拆分服务。


因为每个作为独立部署的服务都需要依赖一个或者多个其他的服务,所以服务之间是存在调用关系的。当服务之间存在调用关系时,功能的迭代是一个很大的问题。


我们举个最简单的例子:


有个应用拆分为两个服务,一个是前端,采用React,Angular.js或者Vue.js的模式去开发;另外一个是后端的API服务,可以使用SpringBoot或者Golang去开发。


很明显,这两个服务之间的关系是前端依赖后端的接口实现来完成自身的功能。所以当后端新增一个接口的时候,要能够迅速地发布到测试环境中供前端做系统集成使用。


当一个系统功能较多,需求变更频繁的时候,频繁的应用发布就带来一个问题。


如果每个应用的发布都要运维来进行的话,那么无论是自动化还是半自动化都没有达到理想的代码即服务的过程,因为总是存在一个开发人员去让运维人员上线代码。甚至于有的时候,希望能够借助于 Git 的 Push 事件来自动触发代码的发布。


这种情况下之下,运维要对开发进行赋能。就是把应用发布这个事情做成一个简单的方案直接交给开发或者测试人员自己完成。


微服务架构方案


我们在日常工作中经常会使用到 Jenkins 作为系统的持续集成和交付的平台,然后配合 Jenkins 丰富的插件去完成相应的应用发布工作。


有的时候还可以结合 Github 或者 Gitlab 系统的推送事件的回调(Webhook)来自动触发代码的构建和发布。


通过上面的手段,我们基本上能够实现了我们开篇所说的 CAAS(代码即服务)的能力。


我相信很多功能都能够这么做了。即使现在没有,但是如果有幸看到这篇文章也会回去搞一搞的。因为这个是真正把运维人员从繁复的发布工作中解脱出来的良方。


自研应用交付平台


我们在上面介绍了一些关于代码快速上线的方案。这些方案本身已经能够解决一些问题。但是我们仍然要回过头去思考一下,这些通用的方案真的完美无缺了吗?


其实不是的。最简单的 Jenkins 的发布需要编写脚本来进行,Git 的相关事件或者流水线发布也需要一堆的配置。一旦涉及到配置的维护和交付,随之而来的错误是 100%会出现的。


曾经在一个发布配置中,我发现 Jenkins 的部署脚本的参数所传入的参数都是直接拷贝的,导致发布上去的两个不同的服务,竟然绑定了一样的域名。如果说遇到了这种错误,而我们又有不同的服务一定是用不同的域名这样先入为主的见解,这个问题排查起来就十分麻烦了。


所以,当我们去使用开源的解决方案的时候,它们本身确实能够解决问题,无论是以什么样的方式。但是由于每个公司内部的业务模式又是不同的,所以当通用的方案无法规避错误的时候,把这种持续性的交付能力开放给开发或者测试就是双刃剑。


我们必须要知道,为了能够控制错误,我们就应该继续抽象问题。把应用持续交付过程中最基本的信息暴露给用户并且提供可控制的配置界面给到用户即可。


剩下的如何对应用进行构建和发布,就是我们需要去开发的交付平台所要做的事情。我们可以利用 Jenkins,Gitlab 或者 Github 的 HTTP API 来和这些独立的系统进行交付,完成应用从代码到打包到最终发布到运行环境的这套流程。



这种自主可控的平台,实际上是在原有的解决方案之上再封装一层,把原来的解决方案中的各个组件作为新的平台的底层支持,并不直接面向用户,而是由平台去面向用户。


这种统一控制的模式能够带来一些之前所没有的好处。当然这里面能解决的第一个问题就是上面提到的域名因为手动拷贝发布脚本而没有变更导致的问题。另外还可以解决在不同需求之下脚本的版本维护和使用困难的问题。


实际的差异化需求是很难用版本号去描述清楚的。拥有统一应用发布平台的好处除了是能够对用户的输入进行校验并且能够简化用户的交互之外,最重要的一点就是能够适用发布需求的变更。


比如说,某些的服务需要增加一些新的配置,这些配置原来的实现方式就是去改发布脚本,但是有了平台之后,我们就可以把这些配置通过数据库存储下来,根据数据库的各个应用的不同配置去实现差异化的发布需求。


这个尤其是在基于 Kubernetes 平台的今天,通过一些 Label 或者是 Annotation 去差异化服务的部署是很常见的。而且在实际生产情况下,应用的资源使用和控制,以及汇总,统计优化等都需要有平台的数据作为支撑才能够进行。


本节小结


微服务架构中由于服务按照功能进行了拆分,不同的服务之间存在了依赖调用的关系。为了能够赋能开发和测试快速实现功能的上线迭代,我们必须能够提供一个统一的 CAAS(代码即服务)的解决方案。


但是传统的基于开源组件的方案并不一定最适合企业中的业务需求,为了能够减少用户的使用成本和出错的概率,我们必须能够基于这些组件开发适应企业实际需求的应用交付平台。


这些平台的存在不仅仅降低了用户的使用成本,还因此积累了应用的发布,运行数据,为我们能够统筹资源规划,实施应用优化,完善监控体系等提供了数据支撑。


所以,有一套自研的应用交付平台还是很重要的,不是简单能让流程跑起来就好了。我们对于应用交付的效率和准确性的执着能够帮我们更好地促进业务的发展。


最后,总结一下:本文通过五个章节就微服务架构中常见的问题进行了简要梳理和分析,希望为大家入门微服务提供一个入口和指南。更多深入的问题欢迎留言和私下沟通。


本文转载自公众号贝壳产品技术(ID:beikeTC)。


原文链接


https://mp.weixin.qq.com/s/yOnh9-VlFCnkb2CtCexm4w


2020 年 6 月 18 日 14:071910

评论

发布
暂无评论
发现更多内容

HTTP 前世今生

大导演

HTTP 前端进阶训练营

重大活动网络攻击面前,京东智联云的攻防之道

京东智联云开发者

云计算 网络安全 云安全

一个数据中台如何算成功了?

薄荷点点

数据中台

BATJTMD,大厂招聘,都招什么样Java程序员?

小傅哥

Java 互联网 面试 小傅哥 简历

熔断原理与实现Golang版

Kevin Wan

go microservice

第八周总结

关于mysqldump,这个参数你可能还不知道

Simon

MySQL timestamp

数字信封加密

莫问

京东11.11完美收官!京东智联云以技术服务助力实体经济

京东智联云开发者

云计算 大数据 云安全

详解快速开发平台与工作流通用组件的设计规范

Philips

敏捷开发 快速开发 企业应用

SpringBoot中的响应式web应用

程序那些事

spring WebFlux 程序那些事 响应式系统 spring 5

线程池 ThreadPoolExecutor 原理及源码笔记

程序员小航

Java 源码 jdk 线程池 并发

charles的使用方法

Yolanda_trying

【涂鸦物联网足迹】涂鸦云平台数据类型和取值约束说明

IoT云工坊

人工智能 云计算 物联网 云平台 数据类型

接口测试文件上传(python+requests)

测试人生路

Python 接口测试

《Java程序员修炼之道》.pdf

田维常

架构师训练营第 1 期第 8 周作业

du tiezheng

极客大学架构师训练营

面对大促DevOps怎么做?这里有一份京东11.11 DevOps备战指南

京东智联云开发者

云计算 DevOps 运维自动化

极客大学 - 架构师训练营 第九周作业

9527

手把手教你撸一个能生成抖音风格动图的gif制作平台

徐小夕

Java css3 GitHub GIF 开源项目

架构师第一期作业(第8周)

Cheer

作业

springboot+java+redis 简单实用的搜索栏热搜,个人历史记录,文字过滤

灰尘子

高性能IO模型:为什么单线程Redis能那么快?

小Q

Java redis 学习 架构 面试

将减少阻力的香蕉法则,运用在软件开发上会产生什么效果?

Philips

敏捷开发 快速开发 企业应用

数据库建表、SQL、索引规范

Bruce Duan

MySQL sql 建表 规范

决策树算法-实战篇

比伯

Java 大数据 编程 架构 算法

面试重灾区——Synchronized深度解析

花火

并发编程 synchronized 内存布局 CAS 锁升级

直播预告 | 云原生在CloudQuery中的应用与实践

CloudQuery社区

数据库 sql 容器 云原生 工具软件

分布式集群如何实现高效的数据分布

vivo互联网技术

分布式 DHT hash 数据存储

usdt支付系统开发方案,币支付交易系统搭建

WX13823153201

不可思议,竟然还有人不会查看GC垃圾回收日志?

田维常

垃圾回收 GC

微服务架构的相关原理和服务交付-InfoQ