从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

1. 选择现成的项目模板还是自己搭建项目骨架

搭建一个前端项目的方式有两种:选择现成的项目模板、自己搭建项目骨架。

选择一个现成项目模板是搭建一个项目最快的方式,模板已经把基本的骨架都搭建好了,你只需要向里面填充具体的业务代码,就可以通过内置的工具与命令构建代码、部署到服务器等。

一般来说,一个现成的项目模板会预定义一定的目录结构、书写方式,在编写项目代码时需要遵循相应的规范;也会内置必要的工具,比如 .editorconfigeslintstylelintprettierhuskylint-staged 等;也会内置必要的命令(package.json | scripts),比如 本地开发:npm run dev本地预览:npm run start构建:npm run build部署:npm run deploy等。

社区比较好的项目模板:

这些模板的使用又分为两种:使用 git 直接克隆到本地、使用命令行创建。

(使用现有模板构建的项目,可以跳过第 2 ~ 7 步)

1.1 使用 git 直接克隆到本地

这是一种真正意义上的模板,可以直接到模板项目的 github 主页,就能看到整个骨架,比如 react-boilerplateant-design-provue-element-adminreact-starter-kit

以 react-boilerplate 为例:

克隆到本地:

git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git <你的项目名字>

切换到目录下:

cd <你的项目名字>

一般来说,接下来运行 npm run install 安装项目的依赖后,就可以运行;有些模板可能有内置的初始化命令,比如 react-boilerplate

npm run setup

启动应用:

npm start

这时,就可以在浏览器中预览应用了。

1.2 使用命令行创建

这种方式需要安装相应的命令,然后由命令来创建项目。

以 create-react-app 为例:

安装命令:

npm install -g create-react-app

创建项目:

create-react-app my-app

运行应用:

cd my-app
npm start

1.3 自己搭建项目骨架

如果你需要定制化,可以选择自己搭建项目的骨架,但这需要开发者对构建工具如 webpacknpmnode 及其生态等有相当的了解与应用,才能完美的把控整个项目。

下面将会一步一步的说明如何搭建一个定制化的项目骨架。

2. 选择合适的规范来写代码

js 模块化的发展大致有这样一个过程 iife => commonjs/amd => es6,而在这几个规范中:

  • iifejs 原生支持,但一般不会直接使用这种规范写代码
  • amdrequirejs 定义的加载规范,但随着构建工具的出现,便一般不会用这种规范写代码
  • commonjsnode 的模块加载规范,一般会用这种规范写 node 程序
  • es6ECMAScript2015 定义的模块加载规范,需要转码后浏览器才能运行

这里推荐使用 es6 的模块化规范来写代码,然后用工具转换成 es5 的代码,并且 es6 的代码可以使用 Tree shaking 功能。

参考:

3. 选择合适的构建工具

对于前端项目来说,构建工具一般都选用 webpackwebpack 提供了强大的功能和配置化运行。如果你不喜欢复杂的配置,可以尝试 parcel

参考:

4. 确定是单页面应用(SPA)还是多页面应用

因为单页面应用与多页面应用在构建的方式上有很大的不同,所以需要从项目一开始就确定,使用哪种模式来构建项目。

4.1 多页面应用

传统多页面是由后端控制一个 url 对应一个 html 文件,页面之间的跳转需要根据后端给出的 url 跳转到新的 html 上。比如:

http://www.example.com/page1 -> path/to/page1.html
http://www.example.com/page2 -> path/to/page2.html
http://www.example.com/page3 -> path/to/page3.html

这种方式的应用,项目里会有多个入口文件,搭建项目的时候就需要对这种多入口模式进行封装。另外,也可以选择一些封装的多入口构建工具,如 lila

4.2 单页面应用

单页面应用(single page application),就是只有一个页面的应用,页面的刷新和内部子页面的跳转完全由 js 来控制。

一般单页面应用都有以下几个特点:

  • 本地路由,由 js 定义路由、根据路由渲染页面、控制页面的跳转
  • 所有文件只会加载一次,最大限度重用文件,并且极大提升加载速度
  • 按需加载,只有真正使用到页面的时候,才加载相应的文件

这种方式的应用,项目里只有一个入口文件,便无需封装。

参考:

5. 选择合适的前端框架与 UI 库

一般在搭建项目的时候就需要定下前端框架与 UI 库,因为如果后期想更换前端框架和 UI 库,代价是很大的。

比较现代化的前端框架:

一些不错的组合:

参考:

6. 定好目录结构

一个好的目录结构对一个好的项目而言是非常必要的。

一个好的目录结构应当具有以下的一些特点:

  1. 解耦:代码尽量去耦合,这样代码逻辑清晰,也容易扩展
  2. 分块:按照功能对代码进行分块、分组,并能快捷的添加分块、分组
  3. 编辑器友好:需要更新功能时,可以很快的定位到相关文件,并且这些文件应该是很靠近的,而不至于到处找文件

比较推荐的目录结构:

多页面应用

|-- src/ 源代码目录

    |-- page1/ page1 页面的工作空间(与这个页面相关的文件都放在这个目录下)
        |-- index.html html 入口文件
        |-- index.js js 入口文件
        |-- index.(css|less|scss) 样式入口文件
        |-- html/ html 片段目录
        |-- (css|less|scss)/ 样式文件目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- sub-dir/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
            |-- ...
        
    |-- ...
    
|-- html/ 公共 html 片段
|-- less/ 公共 less 目录
|-- components/ 公共组件目录
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- ...

单页面应用

|-- src/ 源代码目录
    |-- page1/ page1 页面的工作空间
        |-- index.js 入口文件
        |-- services/ service 目录
        |-- models/ model 目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- module1/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
        
    |-- ...
    
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- components/ 公共组件目录   
|-- ... 

参考:

7. 搭建一个好的脚手架

搭建一个好的脚手架,能够更好的编写代码、构建项目等。

可以查看 搭建自己的前端脚手架 了解一些基本的脚手架文件与工具。

比如:

|-- /                              项目根目录
    |-- src/                       源代码目录
    |-- package.json               npm 项目文件
    |-- README.md                  项目说明文件
    |-- CHANGELOG.md               版本更新记录
    |-- .gitignore                 git 忽略配置文件
    |-- .editorconfig              编辑器配置文件
    |-- .npmrc                     npm 配置文件
    |-- .npmignore                 npm 忽略配置文件
    |-- .eslintrc                  eslint 配置文件
    |-- .eslintignore              eslint 忽略配置文件
    |-- .stylelintrc               stylelint 配置文件
    |-- .stylelintignore           stylelint 忽略配置文件
    |-- .prettierrc                prettier 配置文件
    |-- .prettierignore            prettier 忽略配置文件
    
    |-- .babelrc                   babel 配置文件
    |-- webpack.config.js          webpack 配置文件
    |-- rollup.config.js           rollup 配置文件
    |-- gulpfile.js                gulp 配置文件
    
    |-- test/                      测试目录
    |-- docs/                      文档目录
    |-- jest.config.js             jest 配置文件
    |-- .gitattributes             git 属性配置
  • .editorconfig: 用这个文件来统一不同编辑器的一些配置,比如 tab 转 2 个空格、自动插入空尾行、去掉行尾的空格等,http://editorconfig.org
  • eslintstylelintprettier: 规范化代码风格、优化代码格式等
  • huskylint-staged: 在 git 提交之前对代码进行审查,否则不予提交
  • .gitlab-ci.ymlgitlab ci 持续集成服务

参考:

=================================================

到这里为止,一个基本的项目骨架就算搭建好了。

8. 使用版本控制系统管理源代码(git)

项目搭建好后,需要一个版本控制系统来管理源代码。

比较常用的版本管理工具有 gitsvn,现在一般都用 git

一般开源的项目可以托管到 http://github.com,私人的项目可以托管到 https://gitee.comhttps://coding.net/,而企业的项目则需要自建版本控制系统了。

自建版本控制系统主要有 gitlabgogsgiteagitlab 是由商业驱动的,比较稳定,社区版是免费的,一般建议选用这个;gogs, gitea 是开源的项目,还不太稳定,期待进一步的更新。

所以,git + gitlab 是不错的配合。

9. 编写代码

编写代码时,js 选用 es6 的模块化规范来写(如果喜欢用 TypeScript,需要加上 ts-loader),样式可以用 lessscsscss 来写。

写 js 模块文件时,注释可以使用 jsdoc 的规范来写,如果配置相应的工具,可以将这些注释导出接口文档。

因为脚手架里有 huskylint-staged 的配合,所以每次提交的代码都会进行代码审查与格式优化,如果不符合规范,则需要把不规范的代码进行修改,然后才能提交到代码仓库中。

比如 console.log(haha.hehe); 这段代码就会遇到错误,不予提交:

这个功能定义在 package.json 中:

{
  "devDependencies": {             工具依赖
    "babel-eslint": "^8.2.6",
    "eslint": "^4.19.1",
    "husky": "^0.14.3",
    "lint-staged": "^7.2.0",
    "prettier": "^1.14.0",
    "stylelint": "^9.3.0",
    "eslint-config-airbnb": "^17.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-babel": "^5.1.0",
    "eslint-plugin-import": "^2.13.0",
    "eslint-plugin-jsx-a11y": "^6.1.0",
    "eslint-plugin-prettier": "^2.6.2",
    "eslint-plugin-react": "^7.10.0",
    "stylelint-config-prettier": "^3.3.0",
    "stylelint-config-standard": "^18.2.0"
  },
  "scripts": {                     可以添加更多命令
    "precommit": "npm run lint-staged",
    "prettier": "prettier --write \"./**/*.{js,jsx,css,less,sass,scss,md,json}\"",
    "eslint": "eslint .",
    "eslint:fix": "eslint . --fix",
    "stylelint": "stylelint \"./**/*.{css,less,sass,scss}\"",
    "stylelint:fix": "stylelint \"./**/*.{css,less,sass,scss}\" --fix",
    "lint-staged": "lint-staged"
  },
  "lint-staged": {                 对提交的代码进行检查与矫正
    "**/*.{js,jsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{css,less,sass,scss}": [
      "stylelint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{md,json}": [
      "prettier --write",
      "git add"
    ]
  }
}
  • 如果你想禁用这个功能,可以把 scripts 中 "precommit" 改成 "//precommit"
  • 如果你想自定 eslint 检查代码的规范,可以修改 .eslintrc, .eslintrc.js 等配置文件
  • 如果你想自定 stylelint 检查代码的规范,可以修改 .stylelintrc, .stylelintrc.js 等配置文件
  • 如果你想忽略某些文件不进行代码检查,可以修改 .eslintignore, .stylelintignore 配置文件

参考:

10. 组件化

当项目拥有了一定量的代码之后,就会发现,有些代码是很多页面共用的,于是把这些代码提取出来,封装成一个组件,供各个地方使用。

当拥有多个项目的时候,有些组件需要跨项目使用,一种方式是复制代码到其他项目中,但这种方式会导致组件代码很难维护,所以,一般是用另一种方式:组件化。

组件化就是将组件独立成一个项目,然后在其他项目中安装这个组件,才能使用。

一般组件化会配合私有 npm 仓库一起用。

|-- project1/ 项目1
    |-- package.json
    
|-- project2/ 项目2
    |-- package.json    

|-- component1/ 组件1
    |-- package.json

|-- component2/ 组件2
    |-- package.json

在 project1 中安装 component1, component2 组件:

# package.json
{
  "dependencies": {
    "component1": "^0.0.1",
    "component2": "^0.0.1"
  }
}
import compoennt1 from 'compoennt1';
import compoennt2 from 'compoennt2';

如果想要了解怎样写好一个组件(npm package),可以参考 从 1 到完美,写一个 js 库、node 库、前端组件库

参考:

11. 测试

测试的目的在于能以最少的人力和时间发现潜在的各种错误和缺陷,这在项目更新、重构等的过程中尤其重要,因为每当更改一些代码后,你并不知道这些代码有没有问题、会不会影响其他的模块。如果有了测试,运行一遍测试用例,就知道更改的代码有没有问题、会不会产生影响。

一般前端测试分以下几种:

  1. 单元测试:模块单元、函数单元、组件单元等的单元块的测试
  2. 集成测试:接口依赖(ajax)、I/O 依赖、环境依赖(localStorage、IndexedDB)等的上下文的集成测试
  3. 样式测试:对样式的测试
  4. E2E 测试:端到端测试,也就是在实际生产环境测试整个应用

一般会用到下面的一些工具:

另外,可以参考 聊聊前端开发的测试

12. 构建

一般单页面应用的构建会有 npm run build 的命令来构建项目,然后会输出一个 html 文件,一些 js/css/images ... 文件,然后把这些文件部署到服务器就可以了。

多页面应用的构建要复杂一些,因为是多入口的,所以一般会封装构建工具,然后通过参数传入多个入口:

npm run build -- page1 page2 dir1/* dir2/all --env test/prod
  • page1, page2 确定构建哪些页面;dir1/*, dir2/all 某个目录下所有的页面;all, * 整个项目所有的页面
  • 有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数
  • -- 用来分割 npm 本身的参数与脚本参数,参考 npm – run-script 了解详情

多页面应用会导出多个 html 文件,需要注意这些导出的 html 不要相冲突了。

当然,也可以用一些已经封装好的工具,如 lila

13. 部署

在构建好项目之后,就可以部署到服务器了。

传统的方式,可以用 ftp, sftp 等工具,手动传到服务器,但这种方式比较笨拙,不够自动化。

自动化的,可以用一些工具部署到服务器,如 gulpgulp-ssh,当然也可以用一些封装的工具,如 md-synclila 等

以 md-sync 为例:

npm install md-sync --save-dev

md-sync.config.js 配置文件:

module.exports = [
  {
    src: './build/**/*',
    remotePath: 'remotePath',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
  {
    src: './build/**/*.html',
    remotePath: 'remotePath2',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
];

在 package.json 的 scripts 配置好命令:

"scripts": {
  "deploy": "md-sync"
}
npm run deploy

另外,一般大型项目会使用持续集成 + shell 命令(如 rsync)部署。

14. 持续集成测试、构建、部署

一般大型工程的的构建与测试都会花很长的时间,在本地做这些事情的话就不太实际,这就需要做持续集成测试、构建、部署了。

持续集成工具用的比较多的:

jenkins 是通用型的工具,可以与 githubbitbucketgitlab 等代码托管服务配合使用,优点是功能强大、插件多、社区活跃,但缺点是配置复杂、使用难度较高。

gitlab ci 是 gitlab 内部自带的持续集成功能,优点是使用简单、配置简单,但缺点是不及 jenkins 功能强大、绑定 gitlab 才能使用。

以 gitlab 为例(任务定义在 .gitlab-ci.yml 中):

stages:
  - install
  - test
  - build
  - deploy

# 安装依赖
install:
  stage: install
  only:
    - dev
    - master
  script:
    - npm install

# 运行测试用例
test:
  stage: test
  only:
    - dev
    - master
  script:
    - npm run test

# 编译
build:
  stage: build
  only:
    - dev
    - master
  script:
    - npm run clean
    - npm run build

# 部署服务器
deploy:
  stage: deploy
  only:
    - dev
    - master
  script:
    - npm run deploy

以上配置表示只要在 dev 或 master 分支有代码推送,就会进行持续集成,依次运行:

  • npm install
  • npm run test
  • npm run clean
  • npm run build
  • npm run deploy

最终完成部署。如果中间某个命令失败了,将停止接下的命令的运行,并将错误报告给你。

这些操作都在远程机器上完成。

=================================================

到这里为止,基本上完成了一个项目的搭建、编写、构建。

15. 清理服务器上过期文件

现在前端的项目基本上都会用 webpack 打包代码,并且文件名(html 文件除外)都是 hash 化的,如果需要清除过期的文件而又不想把服务器上文件全部删掉然后重新构建、部署,可以使用 sclean 来清除过期文件。

16. 收集前端错误反馈

当用户在用线上的程序时,怎么知道有没有出 bug;如果出 bug 了,报的是什么错;如果是 js 报错,怎么知道是那一行运行出了错?

所以,在程序运行时捕捉 js 脚本错误,并上报到服务器,是非常有必要的。

这里就要用到 window.onerror 了:

window.onerror = (errorMessage, scriptURI, lineNumber, columnNumber, errorObj) => {
  const data = {
    title: document.getElementsByTagName('title')[0].innerText,
    errorMessage,
    scriptURI,
    lineNumber,
    columnNumber,
    detailMessage: (errorObj && errorObj.message) || '',
    stack: (errorObj && errorObj.stack) || '',
    userAgent: window.navigator.userAgent,
    locationHref: window.location.href,
    cookie: window.document.cookie,
  };

  post('url', data); // 上报到服务器
};

线上的 js 脚本都是压缩过的,需要用 sourcemap 文件与 source-map 查看原始的报错堆栈信息,可以参考 细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息 了解详细信息。

参考:

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

from:https://segmentfault.com/a/1190000017158444

微服务在微信的架构实践

微服务的理念与腾讯一直倡导的“大系统小做”有很多相通之处,本文将分享微信后台架构的服务发现、通信机制、集群管理等基础能力与其上层服务划分原则、代码管理规则等。
背景介绍

首先,我们需要敏捷开发。过去几年,微信都是很敏捷地在开发一些业务。所以我们的底层架构需要支撑业务的快速发展,会有一些特殊的需求。

另外,目前整个微信团队已经有一千多人了,开发人员也有好几百。整个微信底层框架是统一的,微信后台有千级模块的系统。比如说某某服务,有上千个微服务在跑,而集群机器数有几万台,那么在这样的规模下,我们会有怎么样的挑战呢?

我们一直在说“大系统小做”,联想一下,微服务与腾讯的理念有哪些相同与不同的地方呢?通过对比,最终发现还是有许多相通的地方。所以我挑出来讲讲我们的实践。

看过过去几个会议的内容,可能大家会偏向于讲整一个大的框架,比如整个云的架构。但是我这边主要讲的是几个特殊的点。

概览详情

开始看一下我们的结构。全球都有分布,主要有上海、深圳、香港、加拿大几个数据中心。

其中上海服务国内北方的用户,深圳负责南方用户,加拿大服务北美、南美和欧洲,香港服务东南亚、中东和非洲地区。

然后来看看我们的架构:

  • 最上边是我们的产品;
  • 然后有一个号称几亿在线的长连接和短连接的服务;
  • 中间有一个逻辑层,后台框架讲的主要是逻辑层往后这块,包括我们的 RPC、服务分组、过载保护与数据存储;
  • 底层有个 PaxosStore 存储平台。

整套就是这么个体系。微服务很容易去构建,但是规模变大后有哪些问题,需要哪些能力?这里挑出三个点来讲一下:

一、敏捷

希望你的服务很快实现,不太多去考虑。像我们早期互联网业务,甚至包括 QQ 等,我们很注重架构师的一个能力,他需要把握很多的东西。他设置每个服务的时候,要先算好很多资源,算好容灾怎么做。容灾这个问题直接影响业务怎么去实现的,所以有可能你要做一个具体逻辑的时候要考虑很多问题,比如接入服务、数据同步、容灾等等每个点都要考虑清楚,所以节奏会慢。

二、容错

当你的机器到了数万台,那每天都有大量机器会有故障。再细一点,可以说是每一个盘的故障更频繁一点。

三、高并发

基础架构

接下来看看我们的基础架构。

整个微服务的架构上,我们通常分成这些部分:

  • 服务布局
  • 服务之间怎么做一些远程调用
  • 容错(主要讲一下过载保护)
  • 部署管理
服务布局

分两层,一个是城市间。城市之间的数据是相对独立的,除了少数账号全球同步,大部分业务都希望做成电子邮件式的服务,各自有自身的环境在跑,之间使用类似于电子邮件的通信。所以我们选择让每个城市自治,它们之间有一个 200-400ms 的慢速网络,国内会快点,30ms。

而城市内部,就是每个园区是一套独立的系统,可以互相为对方提供备份。要求独立的电源与网络接入。

城市内部会有整套的划分,终端 –>接入层 –>逻辑层 –>存储层 都是完全独立的一套系统。

远程调用

看到很多框架,竟然是没有协程的,这很诧异。早年我们 QQ 邮箱、微信、图像压缩、反垃圾都是一个 web 服务,只有存储层会独立到后面去,甚至用 web 直连 MySQL。因为它早期比较小,后来变大之后就用微服务架构。

每个东西都变成一个小的服务,他们是跨机的。你可以想象一下,每天我们很多人买早餐的时候,掏出手机做一个微信支付,这一个动作在后台会引起上百次的调用。这有一个复杂的链路。在 2014 年之前,我们微信就是没有做异步的,都是同步的,在这么多调用里,A 服务调用 B,那要先等它返回,这样就占住了一条进程或者线程。所以其实 13 年的时候,我们发生了大大小小的故障,很大一部分原因就在这里。

然后 13 年底的时候,这个问题太严重了,严重到,比如发消息的时候,你去拿一个头像之类的,它只要抖动,就可能引发整一条调用链的问题,并且因为过程保护的不完善,它会把整个消息发送的曲线掉下去,这是我们很痛苦的时间。

然后当时我们就去考虑这些方案,13 年的时候抽出 3 个人重新做了一个完整的库 libco。(两千行),实现时间轮盘与事件处理链、常用网络编程模式、同步原语等。它分为三大块,事件驱动、网络 HOOK 和协程机制。

早期是多进程为主,当年切多线程的时候,也遇到一大波修改,后来线程里有了一个线程变量就好多了。如果没有这个东西,你可能要把许多变量改成参数再一层一层传递下去。有了线程变量就好多了。现在我们的协程变量也是这个意义,效果就像写一个宏一样。

另一个是,我们支持 CGI,早期库在 CGI 上遇到问题,所以没有推广。因为一个标准 CGI 服务是基于一些古老的接口的,像 getENV、setENV,就是说你的 coreString 是通过 ENV 来得到的,那么这个我们也把它给 HOOK 掉了,它会根据你的协程去分派。

最难的一个是 gethostbyname 方法,我发现很多人就连在异步编程里,处理 hostbyname 也可能是用了一套独立线程去做,或者你很辛苦地把整个代码抠出来重新写一遍,这个肯定是有很多问题的。所以我们 libco 就把这个 gethostbyname 给完整地支持了。

最后如果你还不爽,说一般业务逻辑可以这么干,那我还有很多后台代码怎么办呢?很多有经验的老的程序员可能要拿着他们那一堆很复杂的异步编程的代码来质疑我们,他们不认为他们的代码已经完全可以被协程所取代了。

他们有如下两个质疑:

  • 质疑性能:协程有很多切换,会不会带来更大开销?
  • 你可能处理几万并发就好,消耗个 1G 内存就行,但是我们这里是处理千万并发哦,这么大的规模,我不信任你这个东西。

这样我们其实是面临了一个问题,因为一些老代码,越是高级的人写的,它的技术栈越深,稍微改动一点代码,就出 BUG 了。

所以我们后来做了两个东西,一个是实际修改了相对简单的异步代码到 libco 里,然后性能更好了。因为在做异步编程的时候,你需要自己去维护很多的数据结构,做你的状态保存,它们的生存期有可能需要很久,你自然地会分配许多内存给它,当然你会用一些内存池去优化它,但是这些是有限的。

但是你用协程的话,很多变量就自然在一个连续的内存里了,相当于一个小的内存池,就比如 if……else……这个你没有必要去 new 一个东西保存状态的,直接放在栈里就行了,所以它的性能更好了。

第二个是,它要求很高的并发。由于协程要一个栈,我们一般开 128k,如果你对这个代码掌控得比较好,可能开 16k,就算是这样,你要开 1 万个协程,还是要 100 多 M 的内存。所以我们后来就在这基础上做了一个可以支持千万连接的协程模式。

Libco 是一个底层库,让你很方便开发,但是大部分开发人员不是直接面对 libco 的,我们花了一年时间把整个微信后台绝大部分逻辑服务、存储服务改成基于 libco,整个配置就直接通过配一台机器上的并发数配 10 倍甚至 20、30 倍,这样子就一下子把整个问题解决了。

过载保护

并发数上去后容易引发另一个问题,早期的时候,后端服务性能高,逻辑服务性能相对弱,很容易被 hold,不可能给后端发起很多连接,不具有“攻击性”,但修改完成后,整个前端变得很强,那可能对后端产生很大的影响。这个时候就要来考虑一下过载保护了。

一般会提到几个点。

1.轻重分离:

就是一个服务里边不要又有重的操作,又有轻的,这样过载的时候,大量的请求都被某些小请求拦截掉了,资源被占满了。

2.队列:

过载保护一般是说系统内部服务在做过去的事情,做无用功。它们可能待在某个队列里边,比如服务时间要求 100ms,但它们总是在做 1s 以前的任务,所以整个系统会崩溃。所以老的架构师会注重说配好每一个服务的队列长度,估算好。但是在繁忙的开发中,是很难去控制的。

3.组合命令式:

后端服务并不是只有一个,上边这个图中的例子,想要调用很多服务,然后 AB 都过载,它们每一个其实都只是过载一点,通过率可达到 80%,但是前端需要这两个服务的组合服务,那么这里就可能只能达到 60% 的通过率。然后后边如果是更多的服务,那么每个服务的一点点过载,到了前端就是很严重的问题。怎么解决呢?

这本书在 12、13 年的时候很火,里边提到了两个对我们有用的点。

  • 一个是“希望系统是分布式的,去中心化”,指系统过载保护依赖每一个节点自身的情况去做,而不是下达一个统一的中心指令。
  • 二是“希望整个控制是基于反馈的”,它举了一些例子,像抽水马桶,像过去炼钢铁的参数很难配,但是只要有一个反馈机制就好解决了。

于是我们构建了一套看起来有点复杂的过载保护系统。

整个系统基于反馈,然后它把整个拒绝的信息全程传递了。看到最右边,有几个典型的服务,从一个 CGI 调用一个后台服务,再调用另一个后台服务,它会在 CGI 层面就把它的重要程度往下传。回到刚才那个前端调用 A、B 服务的例子,使用这样的一种重要程度传递,就可以直接拒绝那些相同用户的 20% 的请求,这样就解决了这个问题。

 怎么配队列?

这个只是反映了生产者和服务者处理能力的差异,观察这个差异,就可以得到一个好的拒绝的数。你不需要去配它多长,只需要去看一个请求在队列里待的平均时间是否可以接受,是一个上涨趋势还是一个下降趋势。这样我们就可以决定要不要去拒绝。那这样几乎是全自动的。你只要配得相对大一点就行了,可以抗一些抖动。在接入之前就评估它,在过去一段时间内平均队列耗时多长,如果超过预支,我们就往下调。这样就把整个系统的过载能力提升了很多。

这是一个具体的做法,我们会考虑两个维度,一个是后台服务,可能服务很多不同的前端,它可能来源于一个支付的请求,经过层层调用,到达后台;或者是一个发消息的服务;它也可能是一个不重要的小服务,如果这个账户服务过载的时候,那么我们可以根据这个表来自动地优先去拒绝一些不那么重要的服务请求,使得我们核心服务能力可以更好地提供。这样整个系统就可以做到很好的过载保护。

数据存储

上边提到一个数据层,那我们是怎么去做数据的呢?

在过去很多年里,我们可能是尽可能去事务化、不追求强一致,一般是采用主备同步的方法。但我们的目标还是强一致的存储。

强一致是说,写一个数据之后,服务器的返回成功不会因为单机故障而丢失。早年我们用的是自己设计的协议,严格来证明的话,没有 Paxos 这么严谨,所以我们在过去一年多的时间内,重新做了一个 Paxos 存储。

它是一个同步复制的数据存储,支持各个园区之间的数据一致性,并且是可以多组多写的,就是说任何一个园区接入,它都可以进行数据的强制读写。另外它并不只是 key-value 模式,它支持 key-value、list、表。在微信这边很少会说完全依赖 key-value 的,因为很多业务都是有列表、表格等的请求,所以很多年前就开始用表格的存储。

Paxos 可用性很高,所以我们就敢做单表有亿行的设计,这样像公众号粉丝等需要很大的,几千万甚至几亿行的记录,就不用考虑自己去分表。并且这个存储可以使用类 SQL 的语句去做,它是完全保证事务的。

它还是插件化系统,不仅支持 LSM,还支持其它存储引擎。

然后它低成本,后台 CPU 有 E3-1230V3,也有 E5-2670 型号的,内存,CPU 与 ssd 之间有一些能力用不上,所以我们系统是可以灵活组合很多不同存储介质的。

这个系统是跑在同城的,也就是上海内部、深圳内部、加拿大内部和香港内部。它们之间的延迟相对较低,几毫秒的级别。这是一个非租约的,没有 leader,不存在切换的不可用期,随时都可以切换任何一个园区。负载均衡这一块我们沿用 kb64 架构,6 台机为一组。因为园区故障少,平时单机时,分摊 25% 的流量,整体比较稳定。6 台为一组时,整个作为一个 set,有很多 set 之间的适用一致性要去做,会有一个很细粒度的伸缩性,比如它可以 100 组扩展到 101 组。

 为什么用这么重的方式呢?

因为希望应用是 简单快速 的,不用假设一个数据写完之后还可能被回退掉,这样只会有很多额外的开销,会有很多问题。比如公众号,他们有很多素材库之类的很重要的存储,如果数据突然丢了,或者说回退了,没有了,那用户投诉是会很严重的。微信账号这边也是这样,如果一个账户注册了,但是这个数据回退了,那也是很严重的问题。

另一个原因是 可用性。在一个传统的主备系统里面,当主机挂掉,面临切不切备机的抉择,然后你会层层请示,说明目前的同步状况,甚至你不知道当前的同步状况,经过很多流程来请示是否切换备机。

而另外,它也不是一个高成本的方案。

 为什么不用 Raft 呢?

Raft 的开源很有价值,它把互联网后台的数据一致性能力提升了很多,就算是一个很小的团队,它也能直接用 Raft 获得一个强一致能力,而这可能就已经超过了许多互联网后台的强一致能力,因为很多后台都是用了很古老的架构,比如长期用到主机架构。

 Raft 与 Paxos 的区别是什么呢?

其实 Raft 和 Paxos 不是一个层面的概念,这个图就是典型的通过一个 log 变更 db 的架构,通过三条 log 一致性做到数据持久强一致性。那 Paxos 在哪里?在一个 log 的某一个 entry 那边,三个点构成一个常量。

那 Raft 是什么呢?它是整一个二维的东西,就是说,基于一个 Paxos 强一致协议做的一条 log,它整个就是一个 Raft。所以我们可以认为 Raft 其实是 Paxos(log)的一种选择。如果你允许绿色部分不存在,那它就不是 Raft。因为 Raft 的设计是你自己做的,它与 Paxos 没关系。

整个 PaxosStore 架构如图:

它包含了很多层,包括缓存和汇聚层、同步复制的组件等。

这一套方案是在线上用了好几千台的,是一个非租约的方案。存储引擎可以自由定制。如果想用大表,那可以用 leveldb。如果想用更强的 LSM,也可以选择。然后我们也有很多 Bitcask 的模型,更适合于内存的 key-value。

由于有几万台机,所以变很重要,我们也基于 BT 做了一套存储方案。它会以园区为根据地,通常一个变更,会以 BT 协议发送到每个园区里,然后园区内部把同机架机器分成一个分组,然后分组内再互传。就我了解,Facebook 和 Twitter、Ebay 都是这样做的。

作者介绍

许家滔,2005 年加入腾讯,见证 QQ 邮箱从百万到数亿用户的整个敏捷开发过程以及架构变迁。2011 年起负责微信后台基础架构,包括分布式存储平台和后台服务框架等,覆盖微信账号 / 消息 / 朋友圈核心存储等,并为公众号 / 微信支付 / 微信企业号等等业务提供组件支持,近两年专注于后台服务质量提升和高性能架构,在数千台机器上面构建了海量高并发 Paxos 存储系统,同时是开源软件 Tencent/libco 负责人。

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

Free Web Hosting