如何构建更健壮的在线系统

0. 背景

Why 为什么要健壮的系统?


1. 为什么测试好好的,上到线上代码一堆bug,一上线就崩溃或者一堆问题?

2. 为什么感觉自己系统做的性能很好,上线后流量一上来就雪崩了?

3. 为什么自己的系统上线以后出问题不知道问题在哪儿,完全无法跟踪?

What 构建健壮系统包含哪些方面?


软件系统架构关注:可维护性、可扩展性、健壮性(容灾)

做出健壮的软件和系统的三个方向:

1. 良好的软件系统架构设计

2. 编程最佳实践和通用原则

3. 个人专业软素质

How 构建健壮系统执行细节?



1、系统架构:
络拓扑是什么、用什么存储、用什么缓存、整个数据流向是如何、那个核心服务采用那个开源软件支撑、用什么编程语言来构建整个软件连接各个服务、整个系统如何分层?

2、系统设计:采用什么编程语言、编程语言使用什么编程框架或中间件、使用什么设计模式来构建代码、中间代码如何分层?

3、编程实现:编程语言用什么框架、有什么规范、编程语言需要注意哪些细节、有哪些技巧、那些编程原则?

一、系统架构与设计遵循哪些关键原则?


0. 用架构师的视角来思考问题


程序员视角更多考虑的是我如何快速完成这个项目,架构师视角是不仅是我完成整个项目,更多需要思考整个架构是否清晰、是否可维护、是否可扩展、可靠性稳定性如何,整个技术框架和各种体系选型是否方便容易开发维护;整个思考维度和视角是完全不一样的。程序员视角是执行层面具体编码的视角,架构师视角是设计师的视角,会更宏观,想的更远。

(比如编程语言选型 PHP vs Golang、Java vs Golang、C++ vs Rust等等)

1. 服务架构链路要清晰

整个服务架构包含:接入层( 关、负载均衡)、应用层(PHP程序等)、服务层(微服务接口等)、存储层(DB、缓存、检索ES等)、离线计算层(不一定包含,一般会以服务层或存储层出现),服务互相不要混了,各干各的活;

服务链路要清晰,每一层各司其职

2. 每个服务都需要考虑灾备或分布式,不能出现单点架构(持续进化)


比如mysql不能只有1个,最少考虑 master/slave架构,保持数据不丢,访问不断,数据量大以后是否做分布式存储等等,redis等同样;后端微服务层同样必须多台服务(通过ServiceMesh等调度,或者是etcd/zookeeper等服务发现等方式);

分布式进化:

初级阶段:

中级阶段:

高级阶段:

3. 每个服务都必须考虑最优的技术选型(最佳实践)


比如PHP框架选择,语言选择都是稳定成熟可靠(高性能API选Swoole、Golang等,业务系统考虑Laravel、Symfony、Yii等主流框架);比如微服务框架,远程访问接口协议(TCP/UDP/QUIC/HTTP2/HTTP3等),信息内容格式可靠(json/protobuf/yaml/toml等);选择的扩展稳定可靠(PHP各个可靠扩展,具备久经考验,超时、日志记录等基础特性);

一些优秀开源软件推荐:(个人最佳实践推荐)

4. 服务必须考虑熔断降级方案


在大流量下,如何保证最核心服务的运转(比如在线课堂中是老师讲课直播重要,还是弹幕或者点赞重要),需要把服务分层,切分一级核心服务、二级重要服务、三级可熔断服务等等区分,还需要有对应的预案;(防火/防火演练等);需要对应的系统支持,比如API 关的选择使用。(OpenResty/Kong/APISIX等,单核2W/qps,4核6W/qps)

5. 运维部署回滚监控等系统需要快速高效



整个代码层次结构,编译上线整个流程,如何是保证高效率可靠的;上线方便,回滚也方便,或者回滚到任何一个版本必须可靠;日志监控、系统 警等等。(常规运维上线系统
Jenkins/Nagios/Zabbix/Ganglia/Grafana/OpenFalcon/Nightingale)

监控系统价值:

监控系统工作原理:

监控系统选择:

6. 编写的接口和前后端联合调试要方便快捷


接口可靠性测试必须充分,并且善于使用好的工具(比如Filder/Postman/SoapUI等),并且对应接口文档清晰,最好是能够通过一些工具生成好API文档(APIJson/Swagger/Eolinker),大家按照对应约定格式协议进行程序开发联调等;

7. 让整个系统完全可监控可追踪


比如对应的trace系统(包含trace_id),把从接入层、应用层、服务层、存储层等都能够串联起来,每个环节出现的问题都可追踪,快速定位问题找到bug或者服务瓶颈短板;也能够了解整个系统运行情况和细节。(比如一些日志采集系统 OpenResty/
Filebeat/Flume/LogStash/ELK)

8. 系统中的每个细节都是需要可以量化的,不能是模糊不明确的


比如单个服务的QPS能力(预计流量需要多少服务器)、并发连接数(系统设置、系统承载连接)、单个进程内存占用、线程数、 络之间访问延迟时间(服务器之间延迟、机房到机房的延迟、客户端到服务器的延迟)、各种硬件性能参数(磁盘IO、服务器 卡吞吐量等)。

9. 让你的应用无状态化、容器化、微服务化


让你的应用可以做到:去中心化、原子化、语言无依赖、独立自治、快速组合、自动部署、快速扩容,采用微服务+容器化来解决。

在面对大并发量请求情况下,在寻求系统资源的状态利用场景,大部分考虑的都是横向扩展,简单说就加机器解决。在docker和k8s的新的容器化时代,横向扩展最好的方法是快速扩充新的应用运行容器;阻碍我们横向扩展的最大的阻碍就是“有状态”,有状态就是有很多应用会存储私有的东西在应用运行的内存、磁盘中,而不是采用通用的分布式缓存、分布式存储解决方案,这会导致我们的应用在容器化的情况下无法快速扩容,所以我们的应用需要“去有状态化”,让我们的应用全部“无状态化”。

微服务化的逻辑是让的每个服务可以独立运行,比如说用户中心系统对外提供的不是代码级别的API,而是基于RESTfu或者gRPC协议的远程一个服务多个接口,这个服务或接口核心用来解决把整个用户中心服务变成独立服务,谁都可以调用,并且这个服务本身不会对内外部有太多的耦合和依赖,对外暴露的也只是一个个独立的远程接口而已。

尽量把我们的关键服务可以抽象成为一个个微服务,虽然微服务会增加 络调用的成本,但是每个服务之间的互相依赖性等等都降低了,并且通过容器技术,可以把单给微服务快速的横向扩展部署增加性能,虽然不是银弹,也是一个非常好的解决方案。

基于容器的微服务化以后,整个业务系统从开发、测试、发布、线上运维、扩展 等多个方面都比较简单了,可以完全依赖于各种自动半自动化工具完成,整个人工干预参加的成分大幅减少。

从单体服务到微服务:

微服务后应用架构变化:

容器简单工作原理:

/*  简单容器底层机制实现模拟演示  说明:主要利用Linux的Namespace机制来实现,linux系统中unshare命令效果类似  Docker 调用机制是:Docker -> libcontainer(like lxc) -> cgroup -> namespace  Code by Black 2020.10.10*/#include <sys/types.h>#include <sys/wait.h>#include <linux/sched.h>#include <sched.h>#include <stdio.h>#include <signal.h>#include <unistd.h>#define STACK_SIZE (1024 * 1024)static char container_stack[STACK_SIZE];char* const container_args[] = { "/bin/bash",  NULL };//容器进行运行的程序主函数int container_main(void *args){    printf("容器进程开始. n");    sethostname("black-container", 16);    //替换当前进程ps指令读取proc环节    system("mount -t proc proc /proc");    execv(container_args[0], container_args);}int main(int args, char *argv[]){    printf("======Linux 容器功能简单实现 ======n");    printf("======code by Black 2020.10 ======nn");    printf("正常主进程开始n");    // clone 容器进程: hostname/消息通信/进程id 都独立 (CLONE_NEWUSER未实现)    int container_pid = clone(container_main, container_stack+STACK_SIZE,         SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET, NULL);    // 等待容器进程结束    waitpid(container_pid, NULL, 0);    //恢复 /proc 下的内容    system("mount -t proc proc /proc");    printf("主要进程结束n");    return 0;}

代码执行效果:

Docker工作机制:

K8s工作机制:

微服务+容器化后的架构:

容器+微服务运维部署架构:

架构设计结束语:


在架构设计中,没有最好的架构,只有适合业务的架构,只有持续优化进步的架构。

二、系统程序实现里哪些关注原则


0. 充分理解你的业务需求


保证理解业务后,整个程序设计符合需求或者未来几个月可以扩展,既不做过度设计,也不做各种临时硬编码。

1. 代码核心原则:KISS(Keep it simple,stupid)


来自于Unix编程艺术,你的东西必须足够简单足够愚蠢,好处非常多,比如容易读懂,容易维护交接,出问题容易追查等等。

因为,长期来看复杂的东西都是没有生命力的。(x86 vs ARM / 微型服务器 vs 大型机 / 北欧简约风 vs 欧洲皇室风 / 现在服装 vs 汉服)

2. 遵守编码规范,代码设计通用灵活


学会通过用函数和类进行封装(高内聚、低耦合)、如何定义函数,缩进方式,返回参数定义,注释如何定义、减少硬编码(通过配置、数据库存储变量来解决)。

3. 设计模式和代码结构需要清晰


比如我们常规使用的MVC设计模式,为了就是把各个层次代码区分开。(M干好数据读取或者接口访问的事儿,C干好变量收发基本教研,V干好模板渲染或者api接口输出;M可以拆分成为:DAO数据访问层和Service某服务提供层);

比如一个主流的MVC层结构图:

4. 程序中一定要写日志,代码日志要记录清晰


(Info、Debug、Waring、Trace等等,调用统一的日志库,不要害怕多写日志)

//程序里关键的日志都要记录(Debug/Notice/Warning/Error 等信息可以打印,warning/error 信息是一定要打印的SeasLog::debug('TRACE_ID:{traceId}; this is a {userName} debug',array('{traceId}'=>9527, '{userName}' => 'Black'));SeasLog::notice('TRACE_ID:{traceId}; this is a notice msg', array('{traceId}'=>9527));SeasLog::warning('TRACE_ID:{traceId}; this is a warning', array('{traceId}'=>9527));SeasLog::error('TRACE_ID:{traceId}; a error log', array('{traceId}'=>9527));

5. 稳健性编程小技巧(个人最佳实践)


a. 代码里尽量不要使用else(超级推荐,Unix编程艺术书籍推荐方法)

//获取一个整形的值 (使用else,共12行)function getIntValue($val) {    if ( $val != "" ) {    $ret = (int)$val;        if ($ret != 0 ) {            return $ret;        } else {            return false;        }    } else {        return false;    }}
//获取一个整形的值(不使用else,共10行)function getIntValue($val) {    if ( $val == "" ) {        return false;    }    $ret = (int)$val;    if ($ret == 0 ) {        return false;    }     return $ret;}

b. 所有的循环必须有结束条件或约定,并且不会不可控

c. 不要申请超级大的变量或内存造成资源浪费

d. 无论静态还是动态语言,内存或对象使用完以后尽量及时释放

e. 输入数据务必要校验,用户输入数据必须不可信。

f. 尽量不要使用异步回调的方式(容易混乱,对js和nodejs的鄙视,对协程机制的尊敬)

6. 所有内外部访问都必须有超时机制:保证不连锁反应雪崩


超时是保证我们业务不会连带雪崩的很关键的地方,比如我们在访问后端资源或外部服务一定要经常使用超时操作。

超时细化下来,一般会包括很多类型:连接超时、读超时、写超时、通用超时 等等区分;一般超时粒度大部分都是秒为单位,对于时间敏感业务都是毫秒为单位,建议以毫秒(ms)为单位的超时更可靠,但是很多服务没有提供这类超时操作接口。

常用使用超时的场景:

Swoole框架里控制超时

//Swoole 里通用超时设置(针对TCP协议情况,包含通用超时、连接超时、读写超时)Co::set([    'socket_timeout' => 5,    'socket_connect_timeout' => 1,    'socket_read_timeout' => 1,    'socket_write_timeout' => 1,]);//Swoole 4.x协程方式访问MySQLCorun(function () {    $swoole_mysql = new SwooleCoroutineMySQL();    $swoole_mysql->connect([        'host'     => '127.0.0.1',        'port'     => 3306,        'user'     => 'user',        'password' => 'pass',        'database' => 'test',        'timeout'     => '1',    ]);    $res = $swoole_mysql->query('select sleep(1)');    var_dump($res);});//Swoole 4.x协程方式访问RedisCorun(function () {    $redis = new SwooleCoroutineRedis();    $redis->setOptions(        'connect_timeout'    => '1',        'timeout'    => '1',    );    $redis->connect('127.0.0.1', 6379);    $val = $redis->get('key');});

通过cURL接口访问HTTP服务超时设置

function http_call($url)   $ch = curl_init($url);   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);   //注意,毫秒超时一定要设置这个   curl_setopt($ch, CURLOPT_NOSIGNAL, 1);       //超时毫秒,cURL 7.16.2中被加入。从PHP 5.2.3起可使用   curl_setopt($ch, CURLOPT_TIMEOUT_MS, 200);     $data = curl_exec($ch);   $curl_errno = curl_errno($ch);  $curl_error = curl_error($ch);  curl_close($ch);}http_call('http://example.com')

访问MySQL超时处理(非Swoole情况),调用mysqli扩展方式:

mysql 默认在扩展层面没有把很多超时操作暴露给前台,所以需要用一些隐藏方式:

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年10月1日
下一篇 2022年10月1日

相关推荐