一、背景
今天给大家总结一下我负责的华为云专项中的内容,很多人不知道为什么会有华为云专线,我简单介绍一下背景,根据集团的国企赛道战略导向,未来的主要客群会是国企客户,国企客户对云的选择会偏向华为云或国资云,那么集团与华为云达成了战略合作,而咱们云链为了切入到国企赛道就必须兼容华为云,也就是说,我们需要把咱们的SAAS服务同时运行在阿里云和华为云上。
刚接触华为云专项时,第一感觉就是无非就是把代码部署到华为云的机器上,跟私有化类似的。但细细想想并不难么简单,主要有以下几个区别:
- 场景不同,私有化一般是单租户的,不管是用户量和流量都是单一租户的,所以性能要求不高,数据库简单用rds就能包的住。但是华为云是需要承载标准saas的服务,未来会有大量的租户在华为云上,所以需要在各项云产品的功能齐全、性能、易用性等都需要对标阿里云,比如华为云的高斯DB对标阿里云的polarDB
- 入口不同,私有化租户可以通过打私有包进行使用,但是华为云作为承载SAAS服务,入口只能同一个标准包,毕竟不能要求华为云的客户下载一个华为云的APP
针对以上区别,需要对华为云对标阿里云的云产品进行方位的评估,以确保SAAS服务能够稳定运行在华为云上,另外从自身业务考虑,需要进行哪些兼容改造,才能同时支持阿里云和华为云。
难就难在了,涉及的面很广:兼容不同云的开发场景、服务的部署发布、sql发布、运行时管理中的监控和告警、日志服务、离线业务、大数据清洗、dmp报表等等,涉及的团队和系统很多:包括云链内部的各个组,对外协同华为云团队给我们做支持,兑现一些没有的功能等。
提示:未来要上华为云的小伙伴,重点看二-1,三-1,四和五章节。
二、多云兼容解决方案
云链SAAS服务进行混合云,我们以租户维度来划分不同的云,也就是一部分租户在华为云,一部分租户在阿里云。从运维的角度,为了保证不同云流量的独立性,我们不同的云采用各自的独立域名,从开发的角度,一套代码兼容两个云,就需要一个云标识来区分,从客户的角度,不管租户在哪个云上对于客户来说应该是无感知的。所以我拉通了云链各组共同确定了以下”多云兼容解决方案”,同时统一对齐各组的在此专项中的工作。
从图中可以看到几个核心信息:
- 因为APP入口只有一个,所以app需要先内置好多个云的整套域名地址,并且根据租户的云标识来切换域名配置;
- 不同的云通过不同的域名来把流量分开,服务在各自的云上实现自闭环,也就是说阿里云的租户,直接使用阿里云的一整套saas服务,华为云亦如此;
- 为了提高兼容能力,只要把配置库中的部分表和通行证数据库进行双向同步,这样一来不管是代码和数据在各个云都是一份完整的备份,可以做到在用户不管往那个云上登录都可以。另外,这样的设计也具备一定的跨云灾备能力,一个云上的基础服务挂了,只要更改基础服务的地址配置就能快速恢复。
- 根据第三点的设定,对于对象云存储来说,应该也是使用各自云自身的云储存产品,如阿里的oss,对应华为云就是obs。考虑到目前ifile组件和app底座均未兼容华为云的obs,而未来租户需要从阿里云迁移至华为云上,对象云存储上的文件需要迁移是一个难题,一是完整的地址是写入到了数据库中,二是没有成熟的迁移工具。(架构组有此工具的开发计划,可以后序关注),所以结论是把阿里云的oss看作是第三方的云存储服务,华为云上也继续使用阿里云的oss;
- 华为云与阿里云在大数据云产品存在一定的差异,比如华为云上没有高性能分析数据库(ADB),无法在华为云上搭建对标阿里云的大数据服务。所以大数据组提出引用天际数芯产品,利用数芯的能力技能满足大数据服务需求,也能提升下云能力,符合集团的战略定位。
多云兼容解决方案是结合各组各子系统兼容华为云的总体方案指导,下面是实现兼容的更多的细节。
1、配置库的同步及云标识
上面多云解决方案核心信息第3点提到,通信证是可以支持任一云登录,实现这一个功能是需要配置库的部分表进行双向同步,目前正在同步的有以下11个表:
mycommunity_config:
- application
- application_product_mapping
- area
- authorization
- contract
- function
- ggcp_tenant_code_mapping
- m_gray_release
- mapping_area_rds
- tenant
- tenant_platform_map
每一个表同步的意义这里不一一说明,有一些表是为了未来迁移租户方便,比如m_gray_release灰度表,如果某一个租户在阿里云上具备灰度功能,迁移到华为云之后,灰度保证不丢失。值得关注的是tenant表,双向同步说明不管在哪个云都是完整的备份,哪个都认识所有的租户,那么问题来了:既然两边云的配置库中tenant表都一样,有如何区分哪些租户运行在华为云上,哪些在阿里云上?答案是”云标识”。
在配置库的rds表中,新增了rds_provider字段
1 | CREATE TABLE `rds` ( |
2、超级APP的改造
混合阿里云与华为云,不同租户运行在不同的云上,两个云使用的域名不同,但是咱们超级APP入口只能是应用市场上的标准版,为了让用户无感知,不会提供两个不同云版本的APP,所以需要超级APP根据用户所登录的租户来决定流量走哪个云的域名。
下面给出架构组 @陈烁 和 @张蓉 给出的改造方案与实现
改造前:
改造后:
概括来说,超级APP需要预先内置好多套不同云的域名集合配置,app启动时默认使用阿里云的域名集合,当用户登录选择租户或者是切换租户时,先获取该租户的”云标识”,然后根据”云标识”来更新window下的环境变量,达到切换域名配置的目的。
超级APP作为前端底座,实现多云域名动态变更之后,window下的环境配置会更新,业务组需要在请求接口的时候动态的获取当前域名,具体可以参考下面 @王伟 在移动质检的实现方案:
获取 url 地址时,使用代理(Proxy)的方式,动态获取当前配置的域名,和对应接口的路径拼接,得到正确的 url
3、Passport的改造
上面第二点解决了APP端单一入口的多云切换问题,但是对于通过浏览器访问的PC端来说,域名也是两套。
考虑到pc后台地址一般都存在于客户的收藏夹里,并且都是阿里云的pc后台地址,那么为了让客户无感知,我们实现了pc端自动跳转,也即是说无论你打开的是阿里云的移动质检pc后台还是华为云的,登录后都能自动根据该租户所属云标识来跳转到对应的域名上。
下面是公共产品组 @黄煜捷 针对passport跳转的时序图
上面时序图可以很清晰的看出整个跳转过程,比较有特色的是,在云错误的场景下,访问云不一致时,会对用户提交的数据进行临时加密处理成凭证,一同携带至正确云上进行解密、校验和会话处理,最终实现自动跳转,简单高效,并且保证了安全性。
三、华为云高斯DB
咱们云链SAAS服务,后端都数据库的性能是有要求的,毕竟租户多,数据量也大,除了优化自身业务逻辑和慢SQL治理,咱们直接采用阿里云PolarDB高性能数据库,对比普通的RDS有6倍的性能提升。
那么华为云需要支撑云链SAAS服务,必然也需要对标PolarDB的高性能数据库,就是高斯DB。
华为云的数据库专家专门为我们讲解过高斯DB的底层架构原理,经过对标评估,基本上也支持存算分离,主从架构等,还有比较多的优秀特性如:1写15读、秒级主备切换、分钟级弹性扩容、NDP算子下推等,同样的配置下,性能是要优于阿里的PolarDB。以上特性不是本文的重点,有兴趣可以去参考资料链接了解一下。
下面聊一下高斯DB使用过程中遇到的问题及解决方案
1. 处理的升级MySQL8.0的兼容问题
咱们云链使用阿里云PolarDB的MySQL版本是5.6,华为云的高斯DB是MySQL8.0,升级的版本跨度比较大,必然存在很多的用法上的兼容问题,在MySQL官方文档中有列出不同版本的不兼容修改,我总结层下面文档,可以下载附件来查看。
我结合官方不兼容修改文档对移动质检的代码进行评估和排查,实际遇到的问题以及实际与到的问题,总结下来目前只遇到下面两类兼容问题:
datetime类型字段与空字符串比较报错问题
我们业务表中基本约定必须带有update_timestamp、created_on和modified_on等datetime类型字段,另外也有不少也是datetime类型的字段,比如有以下表:
1 | CREATE TABLE `q_config` ( |
分别在MySQL5.6和MySQL8.0下面sql执行:
1 | MySQL 5.6 > SELECT name, value FROM q_config WHERE created_on > ''; |
1 | MySQL 8.0 > select name, value from q_config where created_on > ''; |
可以看出来datetime类型字段与空字符串比较时:created_on > ‘’,5.6只会有一个warning,但是8.0会出现报错。
如果把空字符串改成 0000-00-00 00:00:00,则不会报错
1 | MySQL 8.0 > select name, value from q_config where created_on > '0000-00-00 00:00:00'; |
出现这个问题的原因是:将 datetime 值与常量字符串进行比较时,MySQL 首先尝试将字符串转换为 datetime,然后执行比较。在 8.0 ( 8.0.16 ) 之前,当转换失败时,MySQL 将 datetime 视为字符串执行比较。现在在这种情况下,如果将字符串转换为 datetime 失败,则比较失败并显示ER_WRONG_VALUE。这个兼容问题,MySQL官方有定义为Bug:Bug #96361。
注意:DATE、TIME类型也是同样的问题
在SQL_MODE有两个值:NO_ZERO_IN_DATE,NO_ZERO_DATE。这个配置只能避免插入的时候数据的时候出现 0000-00-00 00:00:00 这样的0值,即使设置了该参数,上面的问题仍会报错。
另外说明一下,MySQL的配置中的SQL_MODE参数,不管在阿里云上和华云上,此参数我们都是采用默认为空值。
解决办法
因为PHP弱类型语言,类型不那么严谨,所以在与日期时间相关的业务场景,基本上很难避免查询SQL把datetime类型字段与空字符串比较,尤其是接口参数需要提供日期时间的参数,如果没有判空一路往下传就会到达repository层时直接作为sql的where条件值。
为了不让日期类型不与空字符串比较,最直接的做法就是在repository层的sql执行之前,对日期时间参数进行判空,这是非常容易的事情。当然你也可以controller层就对日期参数进行校验,避免下层传递也可以。
虽然判空很容易,但是历史代码都没有那么严谨,不好保证处处都有判空,需要整个系统所有代码都需要进行排查,工作量就非常的大。我在解决这个问题的时候,有两种思路:
- 从业务角度出发,考虑哪些业务与时间有强关联,比如进度管理,离线数据下载,这种都是与与时间关系强相关的业务,然后针对性地排查这部分业务的接口参数,在入口进行控制。这样相对不容易改出问题,但是容易遗漏。
- 直接在仓储层进行处理,排查代码中用到了所有日期类型的查询,然后在sql执行前进行判空,并且赋予初始值。这种做法工作量大,但是能够覆盖全。
实际解决这个问题我是按第二种思路。下面详细介绍我的解决过程:
第一步:首先需要找到所有的datetime、date类型字段
以通过下面sql,从系统表中查询所有日期类型的字段:
1 | select table_name, data_type, group_concat(distinct column_name) as columns from information_schema.COLUMNS where TABLE_SCHEMA = 'mycommunity_zjfunctest' and TABLE_NAME like "q_%" and DATA_TYPE IN ('datetime', 'date') and COLUMN_NAME not in ('update_timestamp', 'created_on', 'modified_on') group by table_name; |
备注:mycommunity_zjfunctest是质检的测试库名称,”q_”表前缀是质检的表前缀。update_timestamp、created_on和modified_on是我已知的字段类型,所以我排除掉。
第二步:基于第一步的字段结果,寻找规律,并且编写查找代码的正则表达式
比如移动质检会有如下日期类型字段(只列出部分):
start_time、wash_start_date、wash_end_date、remind_datetime、start_on、finish_on、deadline
我们能发现,其实在命名上是可以找到后缀规律的,如_time,_date和_on,有一些是直接一个单词,没有后缀。
考虑yii2框架,通常操作数据库有两种方式:
- 直接拼接sql,然后$db->createdCommend($sql, [])->query()
- 使用query查询器提供的方法,如$query->where(),$query->andWhere()等
实际的业务代码里,两种方式都有,代码写法不同,我们需要针对两种方式编写正则
第一种正则如下:1
(\s|\.)+\b(((wash_start|wash_end|node_update|\w+))_(datetime|date|on|time|at|remark|timestamp|deadline)+|time|deadline|date|birthday)\b\s+(>|<|=|!=|bewteen)
第二种正则如下:1
'(>|<|>=|<=)',\s+'((\w)+\.)?\b(((user_modified|recent_update|node_update|\w+))_(datetime|date|on|time|at|remark|timestamp|deadline)+|time|deadline|date|birthday)\b'
注意:上面的两个正则不能直接使用,需要根据自己的字段来进行修改和调整。 正则的目的是为了找到出现日期类型字段的查询操作。
第三步:基于正则查找代码的查找结果,根据业务逻辑适当地判空并赋予默认值
时间类型的0值格式为:0000-00-00 00:00:00
在此之前,我们先思考一下日期类型查询的不同值的意义,如查询条件格式为 start_time < birthday < end_time:
- ‘1995-01-01 00:00:00’ < birthday < ‘2021-01-01 00:00:00’:得到结果是 95年出生到21年的人
- ‘’ < birthday < ‘2021-01-01 00:00:00’:得到结果是 21年以前出生的人
- ‘0000-00-00 00:00:00’ < birthday < ‘2021-01-01 00:00:00’:得到结果是 21年以前出生的人
- birthday < ‘2021-01-01 00:00:00’: 得到结果是 21年以前出生的人
- ‘1995-01-01 00:00:00’ < birthday < ‘’:得到结果是 没有结果
- ‘1995-01-01 00:00:00’ < birthday < ‘0000-00-00 00:00:00’:得到结果是 没有结果
- ‘1995-01-01 00:00:00’ < birthday:得到结果是 95年后出生的人
上面的结果来看:
- 对于start_time来说,2、3、4结果都是一样的,也就是说,判断start_time为空,直接赋予初始值:0000-00-00 00:00:00 进行查询,是不会影响业务逻辑的。
- 对于end_time来说,空值、初始值与不给条件结果截然不同,判断end_time为空时,需要去掉条件,这样的修改才不会影响业务逻辑。
在兼容这个问题,需要考虑对业务的影响来做出正确改动。下面举个例子:
如图有这样的代码:
参数中有一个$time变量,从查询的条件来看,因为是$time <= update_timestamp,如果$time变量为空,则重新赋值为 0000-00-00 00:00:00 是不应影响业务的。这样的改动是最小的,不需要管下面sql多复杂,只要增加一段判空和赋值的代码即可,比起去掉这个where条件要简单很多,但去掉where条件可能更加合理,我是按照最小改动去兼容的。
但是对于 update_timestamp <= $endTime,如果$endTime为空,则不能通过重新赋值 0000-00-00 00:00:00来查询,去掉条件才是合理的做法,可以通过下面两种2种方式:
1 | // 直接判空 |
另外,$endTime为空时,默认重新赋值为当前时间,在某些场合下也是可以的,具体业务具体考虑。
union all语法问题
如果有这样的sql:
1 | select id, name, value from q_config where name = 'aaa' limit 10 |
在MySQL8.0执行会报语法错误:
1 | MySQL 8.0 > select id, name, value from q_config where name = 'aaa' limit 10 \ |
报错的原因是因为 union all 两边的子句中,带有limit关键字时,会报错。具体原因没有查明,可能mysql8.0的词法分析器修改了,不允许这样的sql。
解决的办法也很简单,只要在union all两边的子句中,添加括号括起来即可:
1 | (select id, name, value from q_config where name = 'aaa' limit 10) |
具体可以业务代码搜索 union all 关键字,然后无脑地两边加上括号即可。
2. proxy层问题
什么是数据库的proxy层?就是在数据库实例之上做一层代理层,在此层可以做流量转发,词法分析等,实现读写分离、主备切换等功能。(这是我的理解,并非标准定义,大概是那个意思,哈哈哈)
阿里云polarDB要先于华为云的高斯DB好几年,在功能的完整性上,polarDB确实要有很大的优势,它提供的proxy层功能也是齐全的,高斯DB在实现proxy层产品上也是学习polarDB的。那么高斯DB的proxy有什么问题呢?目前主要有3个:
- 高斯DB只支持单proxy地址
- 高斯DB的proxy层读写分离功能,仅支持读写模式,不支持只读模式
- 读写分离的一直性问题
高斯DB只支持单proxy地址
先看下面的图,对比一下polarDB与高斯DB的proxy层就能大致了解
阿里云PolarDB的proxy层支持自定义多个proxy地址,每一个proxy能够代理多个节点,这样的特性可以轻松地划分不同组的使用,比如示意图所示:定义一个proxy地址A,自动实现读写分离到1个主节点做写操作和3个从节点负载做读操作,适合把proxy地址A给到业务组使用。另外定义一个proxy地址B,只代理两个从节点实现负载读操作,适合把proxy地址B分配给大数据组使用。这样的划分就非常的清晰。
高斯DB的proxy层读写分离功能,仅支持读写模式,不支持只读模式
proxy层有一个模式配置,分为读写模式和只读模式,意义很好理解,就是读写模式支持读写操作,只读模式只允许读操作,用于做读写的权限管控。这与代理层代理到了主节点或从节点是没有关系的,比如:
读写模式下代理一主一从: 此时读写流量都能进来,自动实现读写分离,写操作到主节点,读操作到从节点上
只读模式下代理一主已从: 此时只能有读流量进来,自动按照读权重进行读负载,一部分读操作在主节点,一部分读操作在从节点
在单proxy下,业务组与大数据组都将使用同一个proxy地址代理的1主多从,高斯DB是不支持只读模式的,那么在读写模式下,是没办法控制大数据组的写操作的。最终提供的解决方案是,通过分配给大数据组一个只读账号来限制写操作。
读写分离的一致性问题
说明会话一致性问题之前,先了解一个场景:
在一个”刚刚写入,马上要查询”的操作中,由于proxy实现的读写分离是往主节点写,往从库中读取,主从同步哪怕是在内网也存在这一定的延时,必然会有出现刚刚写入的数据马上查是查不到的情况。
针对以上问题,PolarDB的proxy是有三种一致性级别可选,用于满足不同的场景需要:最终一致性,会话一致性和全局一致性
最终一致性
PolarDB是读写分离的架构,传统的读写分离只提供最终一致性的保证,主从复制延迟会导致从不同节点查询到的结果不同,例如在一个会话内连续执行如下查询,最后的SELECT结果可能会不同(具体的访问结果由主从复制的延迟决定)。
1 | INSERT INTO t1(id, price) VALUES(111, 96); |
上面的sql语句,最终一致性级别下,会拆开两个语句,insert和update往主节点去做写操作,select语句往从节点去查询。
但是如果把sql语句显式地放在一个事务里面,如这样的代码:
1 | $db = Yii::$app->db; |
则不管读写操作的sql都往主节点上发,这样就能解决主从延时带来的问题
高斯DB只支持最终一致性的级别,貌似也没有什么问题,问题就在于我们的业务代码,基本都是默认使用autocommit来提交事务,很少有主动把单表的更新使用事务。
这样的显式事务,多见于多表同时更新时方便失败时回滚。所以如果要用上最终一致性来解决主从延时问题,就需要修改大量的代码,成本代价太大,甚至有些地方还不好改。
会话一致性
最终一致性是把一个事务的读写sql都往主节点上发,而会话一致性则不需要显式使用事务,而是Session LSN来解决,这里引用官方解释:
在PolarDB的链路中间层做读写分离的同时,中间层会追踪各个节点已经应用的Redo日志位点,即日志序号(Log Sequence Number,简称LSN)。同时每次数据更新时PolarDB会记录此次更新的位点为Session LSN。当有新请求到来时,PolarDB会比较Session LSN和当前各个节点的LSN,仅将请求发往LSN大于或等于Session LSN的节点,从而保证了会话一致性。表面上看该方案可能导致主节点压力大,但是因为PolarDB是物理复制,速度极快。
会话一致性可以不修改任何代码,就能满足我们的场景,而且是能够满足大部分场景,当然代价就是主节点会承担了更多的读压力。
全局一致性
一致性的级别越高,对主节点的压力就越大,所以全局一致性很少使用,会话一致性基本上已经满足大部分场景了,此处就不多解释了,具体可以在参考资料的链接中了解。
注意:因为目前高斯DB的proxy代理层只支持最终一致性级别,其他功能也并不完善,是没办法满足我们当前的需要,所以在530完成功能兑现之前,我们目前放弃使用高斯DB的proxy。
四、定时任务的兼容问题
从”多云解决方案”了解到,不同云在各云内是一整套完整的自闭环服务,所以定时任务也是两边云都有,而租户是不同云有不同的租户,所以定时任务只需要管自身云的租户即可。
所以混合华为云之后,定时任务就会出现兼容问题。简单来说就是,定时脚本在获取全租户code的时候,只要获取自己所在云的租户即可,否则会做对不是自己云的租户跑业务逻辑,会有很多无用功甚至报错。
兼容方法:就是通过环境变量获取云标识,然后结合rds表中的rds_provider字段来对租户code进行筛选
环境变量可以直接在星舟的应用配置上设置(阿里云和华为云上都需要配置):
在config_impl/config.php配置文件中添加:
1 |
|
然后修改获取全租户code的方法:
查询tenant表的时候,增加rds表联查。当前tenant和rds数据量想都很小,增加一个联表,基本没什么影响。
五、SQL发布问题
阿里云和华为云都是支撑云链标准SAAS业务,我们在迭代更新发布的时候,需要两边云都发布,同样地数据库表结构和数据也需要保持一致。
为了sql发布方便,我们可以在SQL发布平台的标签管理中,增加标签来增加便捷性:
对于租户库来说,sql发布直接选上两边的所有租户库即可。但是配置库有一点要注意的,与环境相关的配置,是需要分开发的,因为同样的配置在不同云上是不一样的,比如site表中的域名地址。
六、总结
本文为你介绍了我负责的混合华为云专项,包括多云兼容解决方案,各组很多需要改造的地方,还有当前高斯DB对于我们业务来说存在那些问题和解决方法,还有很多不是很重要但是很细的细节没有一一列出。希望你了解华为云专项,或即将计划上华为云,提供帮助,也希望对你有所启发。
截止至2022年1月17日,已上线了5家租户到华为云上运行。
肝文章不易,看官点个赞再走,比心~