18 如何设计微服务才能防止宕机?

在上一讲里,介绍了构建一个稳健的微服务的具体法则:防备上游、做好自己、怀疑下游, 并介绍了为什么要防备上游,以及一些防备上游的具体手段。

在本讲里,咱们一起来学习,做好微服务自身的设计和代码编写的常见手段。

微服务 CPU 被打满如何排查

在讲解具体有哪些手段可以用来构建一个更加稳固的微服务前,咱们先来看看如何高效、精准地定位问题。下面我将以设计不精良的微服务在线上最容易产生的问题:“机器 CPU 被打满”为例进行讲解。

这个问题也是面试中的高频话题,很多面试者能够回答出其中一二,但距离让面试官满意的答案还有一定距离,下面咱们就一起来看看详细的排查步骤。

一台机器上会部署一至多个进程,它们可能是一个或多个业务应用进程和多个其他工具类进程(比如日志收集进程、监控数据收集进程等)。大概率导致机器 CPU 飙升的是业务应用进程,但仍需准确定位才可得出结论。

在 Linux 系统里,可以使用top 命令进行定位, top 命令可以按进程维度输出各个进程占用的 CPU 的资源并支持排序,同时还会显示对应的进程名称、进程 ID 等信息。

根据排序,便可以确定占用 CPU 资源最高的进程,但此时仍然不知道是哪段代码导致的 CPU 飙升。所以可以在此进程基础之上,做进一步的定位。top 命令支持查看指定进程里的各线程的 CPU 占用量,命令格式为:top -Hp 进程号。通过此方式便可以获得指定进程中最消耗 CPU 资源的线程编号、线程名称等信息。

假设导致 CPU 飙升的应用是基于 Java 开发的,此时,便可以通过 Java 里自带的 jstack 导出该进程的详细线程栈(包含每一个线程名、编号、当前代码运行位置等)信息,具体见下方示例代码:

"thread name" prio=0 tid=0x0 nid=0x0 runnable at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171)

通过第三步定位的线程号和此步骤生成的线程栈,便可以精准确定是哪行代码写的有 Bug,进而导致进程的 CPU 飙升。上述分析是以 Java 应用进行举例,关于其他语言的应用如何导出进程堆栈信息,你可以到对应官网查看。如果有疑问也可以写在留言区,我们一起交流。

如何预防故障

上述介绍了,当微服务的代码编写不优雅,导致 CPU 飙升时,如何快速、准确应对的方法。下面将从微服务的部署和代码编写两个层面介绍一些准则,以便构建一个更加稳固的微服务,前置减少出现故障的概率。

部署层面

首先,微服务及存储需要双机房部署。双机房部署能够进一步提升服务的容灾能力。双机房部署的架构如下图 1 所示:

图 1:双机房部署架构图

上述部署里,同一个微服务分别在两个机房各部署了两台机器。在存储上,数据库的主从分别部署在两个机房里。当出现机房级别的故障,如网络不通时,可以直接将故障机房的机器从微服务的注册中心摘除。其次,如果故障发生在主库所在机房,就需要 DBA 进行协助,对主从数据库的数据对比、订正并进行数据库的主从切换。

双机房部署使得微服务具备了机房级别的容灾能力,当机房出现故障时,可以快速地进行切换,而不用耗费几个小时甚至更久的时间,在一个新的机房进行微服务和数据库的重新部署。但上述的部署里,数据库其实是单机房部署的。因为在实际运行时,只有主库承载读写流量,从库只是跨机房进行数据复制,作为灾备使用。

当真正出现机房故障时,整个微服务仍需停服一定时间,用来等待 DBA 进行主从切换,原则上只在秒级或者分钟级别。这在绝大部分场景里均可满足业务的需要,但有些用户使用高频的场景,如打车、即时通信等软件,需要业务尽可能 7*24 运行,减少或保障不出现业务停服的场景。对于此类需求,可以采用存储按机房多地部署、且每个机房的存储均支持部分用户的数据读写的方案进行升级,此方式在业界有个特有名词,叫作单元化部署的架构,具体架构如下图 2 所示:

图 2:单元化架构图

在单元化架构里,两个机房里的数据库均为主库,它们都承载读写流量。此外,对于用户的请求流量,在网关层进行了转发,一部分转发至机房 A,另外一部分转发至机房 B。假设当机房 A 出现故障时,机房 B 所承载的流量是完全不受影响的,即路由至机房 B 的用户对于故障无感知。

而对于机房 A 里的用户,则可以在网关层进行前置再路由,将所有的请求全部转发至未故障的机房 B。在上图 2 中,有一条两个机房里的数据库主库互相同步的标识线,它是单元化里需要构建的数据同步模块。作用是发生故障时,减少机房 A 里的用户切换到机房 B 的时间。因为机房 A 里的用户可以切换到机房 B 的前置条件是,机房 A 里的数据已经全部同步至机房 B 里,实时的数据同步可以减少故障后 DBA 进行数据同步、对比和校准的时间。

可以看到,单元化架构并不是机房故障后,对于业务完全无损,而是保障一部分用户完全无损来提高高可用能力。

其次,机房内至少部署两台及以上机器。 这里再多啰唆一句,上述第一条要求至少双机房部署,并不是两个机房各部署一台机器即可,而是要在同一个机房里至少部署两台机器,保障机房内机器互相灾备。此方式可以防止当某一台机器故障后,出现整个机房全部失联,进而将调用方的所有的流量都打至另外一个机房,引起请求的性能和稳定性下降,因为跨机房的请求的网络传输时间更长。单机房部署单容器故障时导致的跨机房调用的架构如下图 3 所示,可以看到故障后,调用方的所有流量全部都路由至被调用方的单个机房里。

图 3:机器故障导致的跨机房架构图

再者,不同类型的接口需要单独部署。 在模块二和模块三里介绍过读服务和写服务的特点,这里再复习一下。读服务的特点是调用次数特别大,对于性能要求高。而写服务则是对于稳定性要求特别高,调用次数相比读服务会低很多。假设你在微服务拆分时,没有在垂直拆分时按读写分离的方式将读和写服务拆分开,而是将代码编写在同一个工程里。那么部署的时候,建议将二者的接口拆开部署,拆开后的结构如下图 4 所示:

图 4:读写分离的部署架构

隔离开单独部署主要有以下几点考虑。

写服务对于稳定性要求较高。隔离后,读服务里因为代码 Bug 等因素导致的机器 CPU 飙升、内存占满等问题不会影响到写服务的性能和稳定性。

其次,读服务调用量较高,对于机器 CPU、内存、网络等占用也较高。隔离后,写服务将独享机器资源,性能和稳定性也较好。

最后,微服务的执行线程是根据机器的 CPU 提前设置好的,大小是固定的。读写混合部署时,读请求很容易将微服务框架的执行线程沾满,导致线程枯竭,进而导致写请求得不到执行。此时,通过隔离部署也可以解决此问题。

最后,至少要线程池隔离。 在某些时候,可能读服务的调用次数并不是特别大或机器资源有限,实现不了上述的纯机器隔离。此时,可以实现一个简版的隔离,即微服务框架的执行线程池隔离。现在主流的微服务框架都支持对于接口单独配置一个执行线程,这样在执行时,就可以做到线程池资源隔离,互不影响,具体架构见如下图 5 所示。在某些无法完成机器隔离的场景里,可以使用此方式实现一定程度的资源隔离。

图 5:线程池隔离架构图

代码层面

有很多编写优雅、易阅读、易维护代码的技巧,因为篇幅和专栏定位原因,此处并不会一一介绍,此小节主要聚焦如何编写避免系统故障的编码准则。主要包含以下几点。

第一,不要基于 JSON 格式打印太多、太复杂的日志。

假设有一个特别复杂的类,其中包含了几十上百个字段,同时某些字段也是对象类型,该字段又嵌套了很多对象字段。如果在日志输出时,直接将该类通过 JSON 进行序列化,并进行日志输出,伪代码格式如下:

复杂Object obj=new 复杂Object(); logger.info(JSON.toJson(obj));

如果每一次请求,微服务的代码都会按上述格式打印日志,那么当调用量稍微上升时,很容易将微服务的 CPU 占满,进而导致服务宕机。导致上述现象的主要原因是:复杂的对象在序列化时非常消耗 CPU 资源。建议在打印日志时,按需输出。采用 toJson 方式序列化大对象,很多时候因为简单、粗暴,不需要太多开发量,所以就被研发同学大量、广泛地使用,与此同时也带来了系统宕机的风险。两害相较取其轻,建议采用如下按需的方式输出日志,规避宕机风险。

logger.info("内容1:{},内容2:{},内容3:{}",具体内容1,具体内容2,具体内容3);

第二,需要具有日志级别的动态降级功能。

假如上述按需输出日志的方式还没有被大家广泛接受,你还是习惯使用 toJson 的方式输出日志。那么为了防止打印日志导致机器宕机,需要在日志输出前进行级别判断,使得当日志打印导致机器出现问题时,通过此方式可以将日志进行关闭。具体写法如下:

if(logger.isInfoEnabled()){ 复杂Object obj=new 复杂Object(); logger.info(JSON.toJson(obj)); }

当 toJson 的日志打印把 CPU 占满之后,可以将日志级别调整为更高等级,比如 error 级别,禁止日志输出即可规避问题。此外,更进一步的是,此日志级别调整可以开发动态功能,结合配置中心,动态的修改日志级别,可以实现不重启应用即可生效日志级别修改的功能。

第三,for 循环不要嵌套太深,原则上不要超过三层嵌套。

实践中,for 循环迭代的数据是从数据库或远程 RPC 获取的,获取到的数据量是动态的,可多可少,极端情况下可能多达上千条。此时,三层嵌套下的时间复杂度则为:O(10003)=10 亿。上亿次的代码执行,分分钟就会把微服务打挂,建议在代码编写时,规避此种写法。

第四,多层嵌套需要有动态跳出的降级手段。

假设业务上无法规避上述的多层嵌套,在实现时,需要在嵌套内部开发主动跳出的降级开关。当上述数据量增多时,此方式可以通过开关主动地跳出嵌套,防止机器宕机。

第五,如果使用应用内的本地缓存,需要配置容量上限。

如果不显式地配置本地缓存的容量上限,有可能因为容量暴涨,导致进程 OOM。因此,需要根据机器的内存大小显式地配置本地缓存的容量上限。

本节总结

本讲介绍了设计不规范的微服务会产生的典型线上问题:机器 CPU 被打满的详细处理过程。后续遇到此类线上问题时,你可以参考上述过程进行处理。然后通过部署和代码两个层面讲解了可以规避服务器宕机的一些设计和编码技巧。后续你在设计和开发中,可以进行参考使用。

上述介绍的应用部署和代码编写的原则,都是为了防止微服务出现故障的手段。这些手段,换种说法就是做好自己,防止出现问题。

除了上述介绍的一些原则,你们团队有哪些设计和编码规范呢?欢迎留言,我们一起讨论。

这一讲就到这里,感谢你学习本次课程,接下来我们将学习 19 |如何做好微服务间依赖的治理和分布式事务? 再见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/320388.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android4.4真机移植过程笔记(一)

1、RK源码编译 获取内核源码: git clone git172.28.1.172:rk3188_kernel -b xtc_ok1000 内核编译环境: 从172.28.1.132编译服务器的/data1/ZouZhiPing目录下拷贝toolchain.tar.gz(交叉编译工具链)并解压到与rk3188_kernel同级目…

【项目部署】手把手带你从零部署项目:宝塔 + uwsgi + Django + 腾讯云 + Websocket

1. 前言 哈喽,大家好,我是jiaoxingk。今天带来的是有关Django项目部署的教程。 当我们完成了一个项目作品之后,我们肯定会迫不及待的就准备上线部署啦, 这篇教程将带你从服务器的配置选购,再通过安装宝塔的形式进行项目…

QT程序通过GPIB-USB-HS转接线控制数字万用表

1、硬件准备 1.1、数字万用表 型号 :Agilent 34401A 前面图示: 后面图示:有GPIB接口 1.2、GPIB-USB-HS转接线 2、GPIB协议基础了解 2.1、引脚 8条数据线:DIO1 ~ DIO8 5条管理线:IFC、ATN、REN、EOI、SRQ 3条交握线…

拆单算法交易(Algorithmic Trading)

TWAP TWAP交易时间加权平均价格Time Weighted Average Price 模型,是把一个母单的数量平均地分配到一个交易时段上。该模型将交易时间进行均匀分割,并在每个分割节点上将拆分的订单进行提交。例如,可以将某个交易日的交易时间平均分为N 段&am…

【云原生】Pod 的生命周期(一)

【云原生】Pod 的生命周期(一)【云原生】Pod 的生命周期(二) Pod 的生命周期(一) 1.Pod 生命期2.Pod 阶段3.容器状态3.1 Waiting (等待)3.2 Running(运行中)3…

鸿蒙内核源码分析(消息队列篇) | 进程间如何异步传递大数据

基本概念 队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。 任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务…

Discourse 清理存储空间的方法

Discourse 使用一段时间以后会发现硬盘空间占用非常多。 主要是因为 Docker Image 的问题,如果升级次数越多,空间占用越多。 运行下面的命令: ./launcher cleanup 能够帮助你清理 Discourse 占用的空间。 如下面代码所示: […

牛客热题:单链表排序

📟作者主页:慢热的陕西人 🌴专栏链接:力扣刷题日记 📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言 文章目录 牛客热题:单链表排序题目链接方法一&…

Windows php 安装 Memcached扩展、php缺失 Memcached扩展、Class ‘Memcached‘ not found

在Windows系统下如何安装 php Memcached 扩展 下载dll文件 pecl地址:https://pecl.php.net/package/memcached 根据版本进行选择 : 解压下载的文件后得到了这么样的文件结构: 配置 移动dll文件到相应文件位置 重点: libme…

jdk环境安装

jdk安装 创建软件安装的目录 mkdir -p /bigdata/{soft,server} /bigdata/soft 安装文件的存放目录 /bigdata/server 软件安装的目录 把安装的软件上传到/bigdata/soft 目录 解压到指定目录 -C :指定解压到指定目录 tar -zxvf /bigdata/soft/jdk-8u241-linux-x64.tar.gz -C /b…

【Osek网络管理测试】[TG3_TC3]tSleepRequestMin_L

🙋‍♂️ 【Osek网络管理测试】系列💁‍♂️点击跳转 文章目录 1.环境搭建2.测试目的3.测试步骤4.预期结果5.测试结果 1.环境搭建 硬件:VN1630 软件:CANoe 2.测试目的 验证DUT进入NMLimpHome状态后请求睡眠的最短时间是否正确…

Android --- 消息机制与异步任务

在Android中,只有在UIThread(主线程)中才能直接更新界面, 在Android中,长时间的工作联网都需要在workThread(分线程)中执行 在分线程中获取服务器数据后,需要立即到主线程中去更新UI来显示数据, 所以,如…

NI CRIO 9045 LABVIEW2020

1.labview工程如果要访问CRIO,需要设置以下,否则在项目中连接失败。 2.项目中如果要传文件,需要安装WebDEV 3.使用WebDAV将文件传输到实时(RT)目标 https://knowledge.ni.com/KnowledgeArticleDetails?idkA03q000000YGytCAG&lzh-CN

Mars3d实现用一个button控制一个map.control的显示与隐藏

原生js,想做一个button,控制比如compass的显示与隐藏 点一下显示 再次单击的时候就隐藏掉 写了一个function控制显示隐藏 function addCompass(){ if(compass.showtrue) { compass.showfalse; } else{ compass.showtrue; } } 功能示例(Vue版) | Mars3D三维可视化平台 | 火星…

深入了解C/C++的内存区域划分

🔥个人主页:北辰水墨 🔥专栏:C学习仓 本节我们来讲解C/C的内存区域划分,文末会附加一道题目来检验成果(有参考答案) 一、大体有哪些区域?分别存放什么变量开辟的空间? …

【微服务】网关(详细知识以及登录验证)

微服务网关 网关网关路由快速入门路由属性 路由断言网关登录校验自定义过滤器实现登录校验网关传递用户OpenFeign传递用户 网关 网络的关口,负责请求的路由,转发,身份校验 当我们把一个单体项目分成多个微服务并部署在多台服务器中&#xff…

Redis__数据类型

文章目录 😊 作者:Lion J 💖 主页: https://blog.csdn.net/weixin_69252724 🎉 主题:Redis__数据类型 ⏱️ 创作时间:2024年04月28日 ———————————————— 这里写目录标题 文…

大模型争霸的下一站:不仅是超越GPT-4,更是寻求模型之间的平衡应用

文 | 智能相对论 作者 | 沈浪 知名科学杂志《Nature》发表了一篇关于大模型规模参数大小争议的文章《In Al, is bigger always better?》——AI大模型,越大越好吗?随着大模型应用走向实践,这一问题不可避免地成为了当前AI行业发展的焦点与…

迅雷永久破解

链接:https://pan.baidu.com/s/1ZGb1ljTPPG3NFsI8ghhWbA?pwdok7s 下载后解压 以管理员身份运行绿化.bat,会自动生成快捷方式,如果没有可以在program中运行Thunder.exe

【python】条件语句与循环语句

目录 一.条件语句 1.定义 2.条件语句格式 (1)if (2)if-else (3)elif功能 (4)if嵌套使用 3.猜拳游戏 二.循环语句 1. while循环 2.while嵌套 3.for循环 4.break和conti…