面试微服务
不少人在换工作,也许在面试时,觉得技术交流的都不错,在跟 HR 交流时,薪资谈得也很得意,但是在离开之后,却一直没有 HR 联电的下文,似乎已经忘记,甚至当从来没发生过了。这就是面试时,可能认为回答的不错,但是实际已经给对方留下了反面的影响。其实也许是你的技术可以的,但是跟 HR 谈的薪资可能给不了,也许你的技术还是少了火候,没有与期望薪资对等,尤其在一个二线城市,对薪资那更是。。。你懂的!
今天分享的是微服务的高并发情形,在前面讲过了微服务的理念以及模块的角度划分等。
在微服务高并发下有几点需要考虑:微服务的划分、高并发、数据 DB、中间件或缓存问题、IO 性能瓶颈问题、监控问题、自动化部署问题等。
一、微服务的划分
微服务的划分:前面说过了,服务的划分,可以从水平的功能划分,也可从垂直的业务划分,粒度的大小,可以根据当前的产品需求来定位,最关键的是要做到:高内聚、低耦合。
听到高内聚、低耦合这六个字,面试官也许会觉得这小伙子不错,有一定的技术设计的基础。那么接下来,问题来了。什么是高内聚,什么是低耦合呢?所谓高内聚:就是说每个服务处于同一个网络或网域下,而且相对于外部,整个的是一个封闭的、安全的盒子,宛如一朵玫瑰花。
盒子对外的接口是不变的,盒子内部各模块之间的接口也是不变的,但是各模块内部的内容可以更改。模块只对外暴露最小限度的接口,避免强依赖关系。增删一个模块,应该只会影响有依赖关系的相关模块,无关的不应该受影响。
所谓低耦合:从小的角度来看,就是要每个 Java 类之间的耦合性降低,多用接口,利用 Java 面向对象编程思想的封装、继承、多态,隐藏实现细节。从模块之间来讲,就是要每个模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分尽可能单一。
二、高并发
一个公司一旦做大了,就需要考虑兼容、扩展、压力等问题。高并发是一个常见的词语。然后如何才能保证高并发,这是一个问题。
高并发从以下几个方面来讲:
1. 幂等性
2. 接口代码的规范性
3. 操作 DB 的性能
4. 读写分离操作
5. 服务的横向扩展
6. 服务的健壮性(缓存、限流、熔灾)
幂等性:所谓幂等性,就是说一次和多次请求某一个资源时对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意次执行所产生的效果和返回的结果都是一样的。这种场景是一个很有效的实现高并发的情景,设想,用户充值某个会员,在并发情况下,用户由于误操作,或者由于网络、时间等问题导致重试机制的发生时,可能会触发触发多次交易的扣费,这样给用户一个很不好的体验。此时,就需要接口幂等性来解决这类问题。
幂等性解决方案有以下几种:
(1) token 机制
(2) 接口逻辑实现幂等性
(3) 数据库层处理实现幂等性
token 机制:数据提交时携带 token,token 放到 redis,token 有效时间,提交后台后校验 token,同时删除 token,生成新的 token 并返回。
接口的幂等性:常见的接口幂等性,是定义接口时,加上参数序列号、来源等,序列号与请求来源联合唯一索引,这样可以有效判断本次请求方与请求的序列号,防止重复的请求。
数据库处理:DB 层处理有多种方式,1. 悲观锁,2. 乐观锁,3. 唯一索引、组合唯一索引,4. 分布式锁
悲观锁:所谓悲观锁,是指存在危机意识,事先(查询时)加锁处理,防止事情发生。如:
乐观锁:是指存在乐观心理,只在更新时加锁,乐观锁通常用 version 版本号来控制如:
也可以通过条件限制,这里就使用了组合唯一索引来处理,如:
分布式锁:通过 redis、zookeeper 来设置分布式锁,当插入或更新数据时,获取分布式锁,然后做操作,之后释放锁。
接口的规范性:接口的性能如何,最终还是跟接口的实现逻辑有关,比如代码规范,逻辑实现等,尤其是业务逻辑复杂的情况下,这点需要注意的。
操作 DB:对于业务的持久层,用的比较多的就是 mybatis、hibernate,还有可能是 JPA,无论是哪个,最终都是通过工厂类注入 bean,最后执行 SQL 来操作 DB。所以这里尤为重要的是 SQL 的写法,SQL 的优化决定着操作 DB 的时间以及效果,如果写得不好的话,则会导致死循环,或死锁,或内存溢出。另外测试时,使用真实、规范的数据进行测试,并在测试时不要局限于相同的数据,最后就是并发压测了。
读写分离:当服务足够多,数据足够多时,有可能读与写的占比为:10:1,此时读写应该分离,这样可以有效减少因为读的频繁操作导致的写的性能下降。常见的读写分离的方法有:采用 mycat 中间件方式、amoeba 直接实现读写分离、手动修改 mysql 操作类直接实现读写分离和随机实现的负载均衡,权限独立分配、mysql-proxy(还是测试版本,时间消耗有点高)。
服务的横向扩展:对于服务的请求越来越多时,此时需要对服务进行多节点部署,这样减少单机带来的服务负载压力。
服务的健壮性:服务的健壮性包括缓存、限流、熔灾。
对于缓存,大家都知道,有常见的许多中间件如:redis、kafka、RabbitMq、zookeeper。对于一些 session 等常用 redis 来缓存、共享。对于一些大一点的数据如果嫌弃加载慢,也可以采用缓存机制来解决。
什么叫限流呢?很好理解,就是限制节点的流量,限制服务的请求数。那么如何做到限流呢?常用的限流算法比如有计数器算法、令牌桶算法、漏桶算法。有几种方式:利用 springcloud 组件 zuul 来对请求进行限流,主要是通过谷歌提供的 RateLimiter 结合一些限流算法来限流比较常用。利用 redis 同样可以做限流算法的,甚至可以利用 nginx 直接作计数限流,可以对请求速率进行限制、对每个 ip 连接数量进行限制、对每个服务的连接数量进行限制。如:
其实在 Springboot2.x 中,推出自己的 Spring-Cloud-Gateway 来作网关,同时 Spring-Cloud-Gateway 中提供了基于 Redis 的实现来达到限流的目的。
对于熔灾,或者说熔断,这个在实际的业务当中是很有必要的。比如:用户在某一商城秒杀某一件物品,或在某米商城上抢购某一部手机,在准点抢购时,发现人很多,请求很多,这时,主要是需要有限流机制,同时也需要有熔灾(熔断),给用户留下一个很好的体验的感觉。当用户在点击抢购按钮后,如果当前的请求数很多,需要用户等待,这是需要给一个友好的界面让用户去等待,而不是直接给用户提示请求失败,或者报异常,这样的红色抛出是一个非常不好的事情,用户可能会骂街的,下次也不会逛了。
Spring-Cloud-Gateway 作网关时,过滤器时使用 HystrixGatewayFilterFactory 来创建一个 Filter 实现基于 Route 级别的熔断功能。
三、中间件或缓存问题
随着用户的越来愈多,所有的服务压力也会指数型递增,这时候缓存是一个很好的减轻服务压力的方式。这样可以有效缓冲请求对服务的负载压力。常见的缓存可能是 Redis、MQ(RabbitMQ、RocketMQ)、Kafka、ZooKeeper 等。
Redis 一般主要做 session 或用户信息的缓存,实现多机中 session 的共享。也会用来作分布式锁,在分布式高并发下实现锁的功能,例如实现秒杀、抢单等功能。还会被用作一些订单信息的缓存,防止大量的订单信息被积压而导致服务器的负载很高。总之,Redis 常被用来作为一种缓冲剂使用。
ZooKeeper 也是经常会存储海量数据,例如 Hadoop 中,在使用 YARN 作资源调度时,采用 ZooKeeper 来存储海量的状态机状态以及任务的信息(包括历史信息)。
四、IO 性能瓶颈问题
每个行业的业务也许不同,但是大部分行业是存在存储的,说到最直接的数据库,其他的包括电商、物流、AI 算法等。电商的存储在于页面的数据与后端存储的交互;物流的存储在于物流信息、物品信息的存储;AI 算法在于数据集、模型、镜像文件、训练代码等的存储。整的来说,不管是什么存储,只要跟磁盘、硬盘有关系,就会涉及到 IO 的问题。
对于 IO,在阻塞模式下,经常有线程不够用,就算使用线程池复用线程也无济于事;阻塞 I/O 模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU 利用率很低,即导致系统的吞吐量差,内存占用很高,甚至导致内存溢出。如果网络 I/O 堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠。
什么是 NIO?java.nio 是指 JDK 1.4 及以上版本里提供的新 api(New IO) ,为所有的原始类型(boolean 类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。NIO 核心 API:Channel、Buffer、Selector。
Channel:NIO 的通道类似于流,但有些区别:1. 通道可以同时进行读写,而流只能读或者只能写,2. 通道可以实现异步读写数据,3. 通道可以从缓冲读数据,也可以写数据到缓冲。
缓存 Buffer:缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取,该对象提供了一组方法,可以更轻松地使用内存块,使用缓冲区读取和写入数据通常有这四个步骤:
1. 写数据到缓冲区;
2. 调用 buffer.flip()方法;
3. 从缓冲区中读取数据;
4. 调用 buffer.clear()或 buffer.compat()方法
当向 Buffer 写入数据时,Buffer 会记录下写了多少数据,一旦要读取数据,需要通过 flip()方法将 Buffer 从 write 模式切到 read 模式,在 read 模式下可以读取之前写入到 Buffer 的所有数据,一旦读完了所有的数据,就需要清空缓冲区,让它可再被写入。
Selector:一个组件,可以检测多个 NIO channel,看看读或者写事件是否就绪。多个 Channel 以事件的方式可以注册到同一个 Selector,从而可以用一个线程处理多个请求。
当你调用 Selector 的 select()或者 selectNow() 方法时它只会返回有数据读取的 SelectableChannel 的实例。
另外,就是利用 NIO 实现大文件的分片处理。
五、监控问题
随着业务规模的不断扩大,面临着服务数量不断增加、线上环境日益复杂、服务依赖错综复杂等运维痛点,服务依赖自动梳理、调用实时追踪、异常明细分析、调用链路追踪、实时容量规划、问题根因分析等基本的运维诉求及解决方案就尤其重要。
监控的目的主要包括性能监控、服务的健壮性、运维管理、问题自动分析、动态扩容等,监控的方式也有很多,比如 Springcloud 自己提供的 Dashboard,实现服务的链路跟踪。
K8S 的 Dashboard,可以追踪看到每个服务 pod 的状态以及各项服务的系统指标。除了 k8s 的基本监控外(pod 运行状况、占用内存、cpu)。为了对微服务项目中的各种参数线程池、TPS、QPS、RT、系统负载、thread、mem、class、tomcat、gc、等 jvm 指标进行监控。可以采用基于 K8S 的 promethus job 对业务的 metrics 指标采取收集。同时由于 promethus 支持 grafana 前端 UI 界面。
六、自动化部署问题
随着服务的越来越多,服务的运维管理也是一个麻烦事,如果有一套自动化部署的机制,则可以一键触发自动部署所有微服务,那绝壁是很好的一件事。这块可以参考文章:微服务自动化部署CI/CD 一文,很详细的介绍了自动化部署的实战。
好了,以上所说的,也许对于面试的你会有一定作用的!
结束福利
开源实战利用 k8s 作微服务的架构设计代码:
欢迎大家 star,多多指教。
关于作者
笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 docker、k8s 做微服务容器化,自动化部署等一站式项目部署、落地。Go 语言学习,k8s 研究,边缘计算框架 KubeEdge 等。公众号 程序猿Damon
发起人。个人微信 MrNull008
,个人网站:Damon | Micro-Service | Containerization | DevOps,欢迎來撩。
欢迎关注:InfoQ
欢迎关注:腾讯自媒体专栏
精彩推荐
欢迎关注
版权声明: 本文为 InfoQ 作者【Xiao8】的原创文章。
原文链接:【http://xie.infoq.cn/article/816f7c97ac5b50f41d2ef2043】。文章转载请联系作者。
评论