金融业务-不动产保理业务入门和系统设计

1.保理的概念

保理,从本质上来说,就是应收账款的融资服务。
举个场景:某桂园向某混凝土公司A采购了2000吨水泥,应收账款100万。但由于账期原因,应收账款是按照季度结算。但公司A因为款项没有即时结清,产生了资金周转问题。于是公司A就将应收账款以折扣价转让给了保理商B。保理商B给供应公司A提供了融资,并通知某桂园回款后续不再打给公司A,而是打给保理商自己。季末某桂园将回款打给了保理商B。保理商B在融资和回款的差价里赚到了收益。
整个流程可以参见下图

保理业务流程

在这个最基础的流程中,有三方:

  • 卖方:某混凝公司A,也可以称为债权方、上游
  • 买方:某桂园,也可以称为债务方、下游
  • 保理商:分为商业机构进行的商业保理和银行保理

1.1 正向保理和反向保理

保理这个概念产生的时候,都是由拥有融资需要的卖方主动发起的。但卖方拿到融资却不供货转身跑路的情况,也不是不会发生。这种情况下,买方当然也不会为没有收到的货而白白付钱。保理为了尽量避坏账,会对卖方的资质和规模进行要求。
但现实中很常见的情况是:上游的卖方是中小企业,无法达成资质规模要求,尽调难度也很大;而下游买方是龙头企业,拥有较高的资信程度。为了在这种场景下也能让保理商放心融资,会由卖方(混凝土公司A)找买方(某桂园)做担保,由买方主动发起保理申请。买方为了保证上游供应链的稳定,出面找保理商做担保:公司A确实是我的上游供应商,我们有商务合作。如果你能信得过我的话(大企业的授信担保),就给公司A融资,然后在一定时间段后到我这里兑回款。保理商相信了A的资信,给公司A提供了融资。
在这里出现了一个核心企业的概念。核心企业是供应链中的概念,是供应链中的关键节点,资信程度较高(AA+是基本门槛)。

在保理流程中,核心企业是保理的发起方。核心企业的类型也是区分正向保理和反向保理的关键判断因素。核心企业是卖家,就是正向保理;核心企业是买家,就是反向保理。这里的“正”和“反”指的是相关交易链的方向。

正向保理-反向保理

相比正向保理,反向保理几个优势:

  • 上游量中小企业融资难的问题得以解决
  • 便于建立长期的战略合作伙伴关系,供应链更加稳定
  • 提高了保理业务的工作效率。保理商不再需要着重审核供应商的财务情况。反而需要更关注核心企业贸易背景的真实性,预防核心企业和上游供应串谋欺诈风险
  • 核心企业的资金运用效率得以提升,减少核心企业的财务管理成本

此外,由于保理业务需要债务人确权,所以在正向保理中可能会发生债务人不愿意配合的情况。不愿意配合的主要原因是在转让前,买卖双方的应收账款仅属于商业信用,强势买方在账款到期后,有继续延期支付的可能。在这种情况下,卖方为自身经营利益考虑,也只能被动同意。但是若将应收账款转给保理商之后,商业信用负债就变成了需要刚性兑付的金融负债,到期不还款就会被起诉的可能。所以买方是很不情愿将债权转给保理商的。但反向保理中,由于本身就是债务人发起的,所以基本不会存在类似的问题。

1.2 ABS资产证券化和专项计划

放款给卖方的资金可以是保理商的自有资金,但相较把应收账款捂1年等买方回款,还有其他资金周转率更高的方式,例如将应收账款进行资产证券化(ABS)。
ABS,即“Asset Backed Securitization”(资产证券化),是一种非常常见的融资方式。常见的保理ABS交易架构如下:

ABS交易架构范例

我们扩展一下之前提到的混凝土公司A和某桂园的案例:

  1. 公司A提供给对某桂园水泥,应收账款债权100万。如果不通过保理,正常还款账期是3个月
  2. 为了更快收回款项,公司A将应收账款债权折价至94万,转让给了保理商B,使账期缩短到了半个月
  3. 保理商B以95万元的价格将应收账款债权打包发行ABS(即票面发行利率5%),设立专项计划。通过资产证券化过程中履行合同审核、确权等义务,收取1万元的中介费。如果债务人是某桂园的子公司的话,会加入母公司作为共同付款人来增信
  4. ABS发行一年后,某桂园偿还本息100万元或更多
  5. 价差的5万,在扣除管理费/销售成本/增值税/律师费/评级费/托管费等费用后,剩余的就是证券持有人的收益
  6. 从形式上,这个保理流程中,公司A贴息5万元,但实际由于某桂园的账期从3个月延长到了1年,所以可能会以某种形式承担部分利息,返还至公司A

归纳整个流程,实现了买方/卖方/保理商三方的共赢:

  • 对卖方:实现了快速融资。换个角度,等于公司A做了2.5个月的融资,利息6万,相比从银行融资更容易成功,成本更低。并与核心企业建立了更牢固的合作伙伴关系
  • 对买方:变相拉长了账期。换个角度,等于某桂园以极低的成本融到了9个月的100万元资金。作为牵头发起保理的代价,还可以向卖方后续要求优先供货权等利益
  • 对保理商:获得了低风险的收益

以上范例仅为简化版的范例,实际并不会对单个供应商的应收账款建立ABS。一个ABS产品中通常会包含数十笔到数百笔不等的应收账款。资产池至少包含10个相互之间不存在关联关系的债权人(供应商)。

1.3 再保理

除了将债权ABS化之外,还有另一种处理方式是将转给其他保理商,即再保理。常见的是转给银行,即银行再保理。

与国外相反,国内由大部分金融资源集中在银行,加上政策原因,所以是银行率先开展保理业务。而商业保理是从2012年之后商务部发文批准后才开始发展。由于起步较晚,商业保理公司普遍轻资产,资产负债率较高,无法直接从银行获得贷款。但如果拥有银行认可的优质保理资产,可以通过再保理的形式,将这些资产转计给银行或其他保理商,获得融资。具体流程如下:

再保理

我们从再保理的角度扩展一下混凝公司A、某桂园和保理商B的案例:

  1. 保理商B除了为混凝公司A之外,还为某桂园的一系列供应商提供保理服务,例如混凝土公司X,装修公司Y和园林公司Z
  2. 保理商B想进一步扩大业务规模,希望基于银行C与某园合作的基础上,将自己手头的公司XYZ的应收账款转给银行C,获得融资
  3. 保理商B与银行C签订再保理合作协议,向银行C提供与供应商签的保理合司和票据等材料
  4. 某桂园向银行C出具应收账款转让确认书,确认银行C为新的债权人
  5. 银行C向保理商B发放融资
  6. 保理商B将融资用于扩大业务规模,向更多的公司U公司V公司W提供保理业务

其实从标准的保理概念上,应该称之为“双保理”。即保理商B是卖方保理,银行保理C为买方保理。

2不动产保理业务和保理系统的特点

对于不动产保理ABS业务,实践中通常有以下几个特点:

  • 以反向保理为主:通常采用“1+N”模式。1即供应链核心企业,N为上游供应商
  • 具有类信用债特点:通常直接债务人为由核心企业针对特定地产项目创建的项目公司。对核心企业主体信用依赖强,供应商往往规模小,追索权意义不大,所以大多采用了无追索权保理的形式
  • 平层结构为主:即通常不设外部增信。同样由于强依赖核心企业主的还款力和意愿,对于优先级证券的信用级别没有提升作用,保证担保差额补等外部措施也并不会起到增信作用
  • 通常采用到期一次还本付息的本息兑付方式。主要是由于项目期限短(通常为1年或1年以下)
  • 多采用了储架发行模式和“黑红池”机制。主要是由于存在期限短、单笔金额小、笔数多、同质化高的特点
    • 储架发行,即“一次备案,多次发行”
    • 黑红池机制中“红池”指前期审批报备阶段所使用的基础资产池,而“黑池”则是在证券发行时使用的基础资产池

这些特点使得不动产保理系统在设计过程中产生了以下几个特点:

  • 需要包含供买方/卖方/核心企业使用的外部客户门户系统
  • 核心企业成为买方和资产的必填要素
  • 需要与核心企业进行系统对接
  • 保理产品的设计较单纯(相较其他一些公私募产品)
  • 尽调管理以核心企业为查询维度
  • 还款管理和催收管理弱化

3.资产的属性及和其他业务对象的关联关系

不动产保理中的核心概念是资产
资产就是应收账款。资产本身的主要因素包括:发票编号、发票金额、资产转让金额等。

资产与众多业务对象之间都有关联关系:

资产-业务对象关联关系

接下来围绕资产和不同业务对象的关联关系展开描述。

3.1 供应商/项目公司/核心企业

这里的项目公司即上文提到的买方(债务人),供应商即卖方(债权人)。所以资产和三者的关系是:

  • 供应商卖出资产
  • 项目公司买入资产
  • 核心企业增信资产

在资产导入阶段,如果资产对应的供应商和项目公司未进行准入审核,会生成审核任务,并通过短信通知供应商和项目公司登录客户门户自助补充资料(实物、银行账户、联系人等)。完成准入审核是合同签订的必要前置条件。

准入审核的关键信息包括:公司名称、统一社会信用代码、用印方式、营业执照、资质证书、法人身份证、授权用印经办人授权书影像、授权用印经办人身份证影像等。其中对非结构化数据(例如营业执照的影像资料)的审核也被称为实物审核。

在新增项目公司和供应商后,内部也会开展尽调(尽职调查)。除了对公司的尽调外,也会对资产进行尽调,以确保不是通过虚假交易骗保(理)。
在企业准入成功后,会在电子签章系统里为其注册账号,用于后续的合同签署。

3.2 产品/投资项目/项目/资产包

产品/投资项目/项目/资产包,这几个概念从上往下,从抽象到具体。

产品

产品可以是针对某个核心企业的业务模式,比如“某桂园私募ABS反向保理”,“某科再保理”等。

产品的关键信息包括产品类型、还款方式、宽限期、资金规模、债权期限天数、利率范围等。其中产品类型是针对主体(正向/反向保理)+资产处置方式类别。例如:

  • 公募ABS反向保理
  • 私募契约型基金反向保理
  • 私募信托反向保理
  • 供应链ABS正向保理
  • 银行再保理

投资项目

投资项目,是保理商公司内部进行投决(投资决策)的投资活动单位。
所以投资项目的关键因素包括立项会、投决会等会议的信息、审批规模、投资团队、资金来源等。

项目资产包

根据保理的不同阶段,项目可以分为保理项目和资产处置项目。
保理项目是合同签署的对象单位。对于保理项目,关键信息包括签约方式,资产到期日、折现期、折价率等。
资产处置项目是将资产金融化的对象单位,会补充对应的金融产品的信息,包括金融产品代码(例如基金代码)评级、服务费、律师事务所、托管人、评级机构、专项计划户名账号等等。

资产包

项目和资产包对应。保理项目和资产处置项目分别对应预备资产包和(处置)资产包。
资产包是应收账款的组合。会从预备资产池中选出符合要求的应收账款进行打包处理。上文也提到过,如果要包装为ABS产品的话资产包中至少需要包含10个相互之间不存在关联关系的供应商。当资产包中的资产确定,进行合同签署后,就会转为(处置)资产包。后续资产包中的资产就不可再发生变化。

3.3 中登登记

《民法典草案》第七百六十八条规定:应收账款债权人同一应收账款订立多个保理合同,致使多个保理人主张权利的,已经登记的先于未登记的取得应收账款;均已经登记的,按照登记时间的先后顺序取得应收账款,均未登记的,由最先到达应收账款债务人的转让通知中载明的保理人取得应收账款;既未登记也未通知的,按照保理融资款或者服务报酬的比例取得应收账款。

这里的登记,指的就是到国家公认的中登网进行登记。
中登网,全名中国人民银行征信中心动产融资统一登记平台。中登网的主要作用是用于以融资为目的的动产登记公示。
对于保理业务来说,中登网的主要作用是收质押登记和应收账款转让。如果没有在中登网上进行登记,那么万一项目公司背着保理商,偷偷将应收账款进行多次质押引起纠纷,在法律上就会处于不利地位。

当资产成功导入且包含发票,就需要进行中登查询和登记。
首先进行中登查询。中登查询不通过的话就会进入到退单流程;而中登查询通过的话就进入登记流程。
中登登记的关键因素包括:应收账款的出让人信息(供应商企业信息)、资产信息、受让人信息(保理商企业信息)等。

3.4 合同

《民法典草案》第七百六十二条规定:保理合同的内容一般包括业务类型、服务范围、服务期限、基础交易合同情况、应收账款信息、转让价款、服务报酬及支付方式等条款。
合同签约包括两种方式:纸质和在线。保理商分别于如果是在线签,为了确保合同的合规,会通过第三万电子签章服务进行合同的签署和生成(如E签宝、上上签)。

3.5 放款还款

当资产和实物审核通过,并在中登网登记成功,内部合同用印完毕后,即可进入向供应商的放款申请审批。由于放款申请所需的绝大部分信息在该步骤之前都已补充完华(供应商信息/资产转让金额),所以只需要补充支付方式(商票支付/现金支付)即可。
当处置资产的到期日到达后,会创建还款登记和确认任务。还款登记的主要因素包括本次还款日期还款金额、还款凭证等。还款登记完成后,就可以进行财务流水匹配和制证处理。

4. 保理流程

经过前一章的介绍,我们就可以将资产在整个保理流程中的流转过程梳理出来

  1. 首先在前置阶段,预先配置好资产所属和核心企业以及上层的产品/投决项目信息
  2. 在投前(保理项目)阶段,进行供应商和项目公司的信息补充和审核。同步进行资产导入和审核,并将资产到中登网上进行登记
  3. 进入合同签署,资产包里的资产固定,进入投后(处置项目)阶段。在该阶段进行放款资产出售以及收款

在保理流程的不同阶段,资产包的类型也会发生变化。在投前阶段,是从预备资产池生成预备资产包,并退回审核不通过的资产;在投后阶段,预备资产包变为处置资产包

5. 参考资料

正向保理和反向保理有什么不一样

供应链金融之反向保理资产证券化模式

浅析“再保理”业务

一文读懂:房地产供应链应付账款ABS

本文永久链接 [ https://galaxyyao.github.io/2020/08/01/金融业务-不动产保理业务入门和系统设计/ ]

运维-运维体系标准化之故障管理

本文是极客时间《赵成的运维体系管理课》的读后体会之二 。

1.对故障的认识

ITIL的10个重要的IT管理关键模块之一就是故障管理。
故障永远只是表面现象,其背后技术和管理上的问题才是根因

即当技术和管理上的问题积累到一定程度后,就会以故障的形式爆发出来。所以不能仅将眼光限于故障本身和直接责任人。

  • 管理者要先自我反省:员工只是执行者,管理者的责任永远大于执行者
  • 强调用技术解决问题,而不是单纯地靠增加管理流程和检查环节来解决问题
    • 短期可以辅以一些管理措施,比如靠宣传学习必要的Double Check/制定复杂操作的Checklist等。但是这些只能作为辅助手段,一定不能是常态
    • 随着系统复杂度越来越高,迟早有一天会超出单纯人力的认知范围和掌控能力,各种人力的管理成本也会随之上升,所以最终必须将这些人为动作转化到技术平台中去

2.故障的定级

故障需要有标准化的流程来指导我们的处理过程。

这里有个关键组织:故障应急小组。这个组织有4个职责:

  • 制定故障定级定责标准
  • 对线上故障做出定级和定责
  • 跟踪线上故障处理
  • 组织故障复盘

故障应急小组需要有个负责人。在部分公司,这个负责人属于研发效率团队。
定级定责标准等同于法律条款,而这个角色等同于法官。法官依法办事,做到公平公正。
现实情况中,因为各方受到故障的影响不同,对故障影响的理解也不同,所以复盘过程中经常会出现下面这两种争执场景:

  • 技术支持判定故障很严重,但是责任方认为没什么大不了的,不应该把故障等级判定到如此之高
  • 技术支持认为故障影响较小,但是受影响方却认为十分严重,不应该将故障等级判定得这么低

所以需要有严格而明确的判定标准。故障定级标准的目标是要判定故障的影响程度,使各相关利益方能够基于统一的标准判断和评估。
故障等级常见可以分为PO-P4共5个级别。PO最高P4最低。定级主要看3点:功能的核心程度,影响面,以及影响时间。核心程度有一些共通的标准(例如登录),也有各系统独有的业务衡量标准,所以需要基于每个系统与业务部门分别制定。影响时间是包含故障发生到完全解决的总时间。根据实际况,也可以调整为只计算工作时间的时间长度。如果影响时间超过一定时长,要进行故障升级。
P故障通常是两个或以上P1故障的叠加造成。

2.1 故障定级范例

下面是两个定级参考范例。首先是交易系统,主要以钱为衡量指标。

故障定级-交易

另一个是IM即时通信App的故障定级标准。

故障定级-IM

再次强调一下,为了避免日后引起争执,需要将定级标准在业务部门、产品团队、开发团队、测试团队和运维团队之间进行逐点的细节讨论,并达成最终一致的认可。
这个标准可能覆盖不到有些特例,这个时候需要由应急小组的负责人根据已达成一致的标准+自己的经验进行独立裁量。同时,在每季度或半年对标准进行一轮修订。需要注意的是要对故障应急小组,特别是应急小组的负责人树立绝对的话语权和决策权的制度。

3.故障应急处理

故障发生后可能会产生很大的外部压力,并传递到研发团队。如果没有很好的故障应对措施,很容易陷入慌乱。

3.1 故障应急的原则

在故障应急状态下,坚守的第一原则是:优先恢复业务而非定位问题。
这需要事先有充足的预案准备以及故障模拟演练,也涉及各种稳定性保障措施,例如扩容,开关,限流降级等。

3.2 故障应急流程

故障应急流程由故障应急小组来主导。对外同步信息,包括大致原因,影响面和预估恢复时长,同时屏蔽各方对故障处理人员的干扰;对内组织协调各团队相关人员集中处理。

故障的应急流程主要分为以下几个步骤:

  • 确认故障的有效性
  • 登记生产缺陷
  • 将故障上报到可用性保障群里
    • 故障的原因排查和讨论在该群里或者单独拉一个独立的故障处理群处理。如果相关人员比较集中在一个办公场所,则集中到会议室
  • 对于处理时间比较长的故障,应急处理小组每隔15-30分钟对相关业务部门同步一次故障处理进程,并判断是否需要升级故障
  • 确定故障处理方案,包括:正常业务流程处理提交数据修改单/修改配置/回滚/紧急版本/放到大版本
  • 在故障确认处理完成后,关闭生产缺陷
  • 组织故障复盘,产出故障分析报告,将问题记录到事件管理。故障分析报告需要同步给技术副总监、PMO,以及其他的产品、开发、测试和运维,以便后续吸取教训
  • 根据事件管理的记录,进行故障数据分析
    • 分析角度包括:每月故障数对比、每月故障处理时间对比近两月故障等级占比分布、近两月故障类别占比分布、近两月故障来源对比和近两月各业务组故障数对比

3.3 故障的信息通报

对于每一级故障的知会人员的标准参考如下:

  • P0/P1:技术总监/PMO
  • P2:技术副总监/PMO/测试主管/运维主管
  • P3/P4:技术经理/产品经理

4.故障的复盘

首先要明确复盘的目的。复盘的目的是为了从故障中学习我到我们技术和管理上的不足,然后不断改进。切总将复盘过程和目的搞成追究责任或实施惩罚,这对于团队氛围和员工积极性的打击是非常大的。
在复盘过程中,故障应急小组要起到关键作用,组织复盘会议。
对于低级别的生产事故,在晨会夕会之后顺带进行;对于高级别的生产事故,需要专门安排时间进行。

复盘会议的环节包括:

  • 故障的回顾
    • 包括故障发生时间点,故障影响面,恢复时长,主要处理人或团队
  • 故障处理时间线回顾
    • 故障应急小组在整个过程记录时间点,以便真实再现整个故障处理过程
  • 针对时间线合理讨论
    • 比如为什么没有告警而是用户反馈的,响应时长是否符合规范,是否有预案和预案执行完成度,测试环节为什么没有发现等
    • 故障应急小组负责人注意控制场面,务必注意对事不对人,及时干预和警告,避免演变成批斗会
  • 确定故障根因
  • 故障定级定责
    • 对于高级别生产故障定责时可以仅少数相关人在场时进行,考虑责任人个人感受
  • 产出故障分析报告
  • 将问题记录到事件管理

对故障根因的讨论可以诸如:

  • 是否是人员对业务不熟悉导致?
  • 是否有人为操作导致?如果是的话,是否能改为自动化?
  • 是否在代码静态扫描中包含但被忽略了?
  • 为什么容量不足没有更早发现?
  • 为什么没法快速定位?是监控不够,还是告警太多人员麻木?
  • 管理上,人员的on call机制是否及时?应对故障的协作方式上是否还能改进?

此外,按季度、半年和全年的周期,需要进行周期内的故障案例总结会。总结会的目的包括:

  • 分析故障趋势,观察是否需要进行人员安排的调整
  • 发现共性的问题,贡献给整个研发团队

5.故障的定责

定责的目的是为了责任到人,并且使责任人能够真真切切地认识到自己的不足之处,能够主导改进措施的落地。
相比故障复盘的对事不对人,定责就是对人不对事了,所以不能一刀切,不能上纲上线,一定要慎重。
对于故障的定责方式,也要根据故障的类型来划分。

5.1 高压线规则

有一类是绝对不允许触碰的底线。对于这类高压线规则,需要让每个成员牢记在心,并经常重复提醒。例如:

  • 未经发布系统,私自变更线上代码和配置
  • 未经授权、严格的方案准备和评审,私自在业务高峰期进行硬件和网络设备变更
  • 未经授权,私自在生产环境进行调测性质的操作
  • 未经授权,私自变更生产环境数据

通过高压线去加强安全稳定意识,目的是要让每一个人对线上都心存敬畏。从经验来看,绝大多数的严重故障都是因为无意识或意识薄弱导致的,并不是因为单纯的技术能力不足等技术因素。很多人事后复盘时候最常讲的话就是:“我以为是没问题的,我以为是没影响的。”其实恰恰就是因为这种“想当然”,导致了严重故障。

对于高压线问题,碰一次就要疼一次。

5.2 鼓励做事,而不是处罚错误

Google的专家有一句名言:理解一个系统应该如何工作并不能使人成为专家,只能靠调查系统为何不能正常工作才行。

每个人的技术能力提升,基本都是伴随着大大小小故障的发生、处理、复盘和改进。虽然我们不希望有故障发生,但是真的没有了故障,我们也就没有了真刀真枪实战成长的机会。我们对待故障一定要客观和辩证地理解,特别是对于管理者来说,对于故障,一定要有容忍度,一定要有耐心。
我们的团队和人员,在这样一次次痛苦的经历后,各方面的能力都得到了锻炼,素养也一定会有大幅度提升。所以,对故障有容忍度,有耐心,我们的团队就会变得越来越强,对于故障的应对也会变得更加游刃有余。而一出故障就劈头盖脸地把团队和责任人骂一通,并且还要严厉处罚的方式,最后的效果就是严重打击士气,适得其反。
特别是以下这些原因造成的故障:

  • 员工积极主动地承担了一些极具挑战性的工作,需要尝试某个新技术或解决方案
  • 业务高速发展时期,业务量成指数级增长时,团队人员能和经验水平整体上还没法很好地应对。这个时候可能任何一个小变动都是最后一根稻草

何况,如果不出问题,可能很多主管压根都没有关注过员工在做的事情,过程中是否有困难,是否需要支持等等,这本身就是管理者的失责。
员工努力做事的积极性一旦被打击,变得畏首畏尾起来,也就谈不上什么技术进步和突破了而且想要再恢复起来也会非常困难,最终很大概率上会导致优秀人才流失。

5.3 定责和绩效非强挂钩

在故障后直接谈及处罚,员工的情绪很可能会消极和抵触。例如“反正都是我的错,你说咋样就咋样”,或“凭什么罚我却不罚别人,又不是我一个人的问题”。员工害怕、甚至拒绝承担责任,宁可少做不做,也不愿多做多错,团队沟通成本上升,运作效率自然下降。特别是一个故障如果是涉及多方的,扯皮推诿就开始了,都想着把责任撇干净,甚至当众相互指责,这个负面效应杀伤力极大。
所以可以考虑将定责放到季度、半年为维度,根据事件管理中的记录来整体判断。如果员工整体的表现都是不错的,甚至是突出的,说明员工已经改正或者那件事情确实是偶尔的失误导致,这种情况下员工仍然会有好的绩效。但如果是频繁出问题,这种情况就基于事实反馈,也会更加容易沟通。

本文永久链接 [ https://galaxyyao.github.io/2020/07/30/运维-运维体系标准化之故障管理/ ]

运维-运营体系标准化之配置管理CMDB

本文是极客时间《赵成的运维体系管理课》的读后体会之一。

运维配置管理实践中一些混乱现象

在公司的运维配置管理实践中,存在一些混乱的现象:

  • 信息安全收到fastjson的安全问题报告,但报告中的服务器对应的系统却有错位
  • 信息安全收到了报告,发现tomcat的某个版本出现了漏洞需要升级,但不知道到底影响到哪些系统,只能逐个请每个系统的负责人判断
  • 部分服务器(特别是开发坏境服务器)已经基本可以确认不再被使用,但服务器资源没有回收,每月持续占用硬件预算
  • 某系统上线后发现中漏配置了一个域名,导致部分用户打开首页报错
  • redis缺少规划,多个系统公用一个redis集群的情况下,无法根据键(key)来区分每个系统占用了多少缓存。遇到内存不足的情况很难深入排查是哪个系统导致
  • 生产消息队列中不少queue堆积着大量消息,但不知道是否还在用,不敢随意删除

这些情况很可能短期甚至长期内不会直接导致什么问题。可能就是让信息安全整理材料多费一点时间,开发排查多绕一点弯路,或多花一些硬件维持或扩容费用。但有些时候这些问题就是给未来出现的生产问题埋下隐患。
硬件的归属,服务器的使用情况,应用域名管理,软件版本,应用与中间件的关联等等,都属于广义上的配置管理。所以归根到底,是配置管理的混乱。

ITIL和配置管理CMDB

在ITIL(Information Technology Infrastrueture Library)中,有10个重要的IT管理关键模块。其中配置管理(CMDB)通常被认为是其他IT流程的基础。
CMDB(Configuration Managoment DataBase),配置管理数据库,是与IT系统所有组件相关的信息库。它包含IT基础架构配置项的详细信息。
在传统运维时代,CMDB的核心对象是资源,即网络和硬件设备。但在云计算和互联网运维时代,CMB的核心已经转变为了“应用”。随着微服务架构的推广,以应用为核心的注册中心、缓存、消息队列、数据库等都需要纳入配置管理的管理范畴。

以应用为核心的配置管理标准化可以包括:

  • 元数据属性:系统名、 BA Owner/开发Owner/运维Owner
  • 环境属性:拥有几套环境
  • 逻辑实体属性:架构评审和变更设计
  • 硬件和网络属性:服务器配置、SLB、域名、ip等
  • 代码属性:编程语言、代码库地址、需求空间地址
  • 应用日录属性:日志日录、应用目录、临时目录等
  • 应用配置属性:端口号、jvm参数等
  • 中间件属性:web容器、注册中心、缓存、消息队列、数据库等

最终目标是能达成传统CMDB和应用视角CMDB的统一。
CMDB统一

也有开源的CMDB,比如腾讯的蓝鲸智云
可以参考它的架构

腾讯蓝鲸架构

CMDB的维护和流转

CMDB并不能只靠运维团队内部封闭来做,而要站在怎么能够打造和发挥出整个技术架构体系运维能力的视角。有部分信息也需要开发来提供,例如消息队列的queue名等。所以CMDB需要跨团队协作。一方面运维团队要主动出击,去沟通,去推进;另一方面,必须能得到上级主管甚至是更高层技术领导的支持。
配置数据也需要保持持续维护。过时的配置数据等于没有,甚至更差。这个首先需要在所有运维人员的重要性认识上保持统一。此外,也对每个应用的负责运维和研发人员提出相应的问责机制。
和货币类似,只是维护信息不会产生价值。只有把信息流转到开发,测试和信息安全等角色,对整体的研发效能和故障率做出贡献,才能使配置管理免于成为运维的纯负担。

本文永久链接 [ https://galaxyyao.github.io/2020/07/29/运维-运营体系标准化之配置管理CMDB/ ]

管理-远程办公项目管理经验总结

疫情期间积累了一些远程办公条件下的项目管理经验,稍微整理一下。我司从企业文化到网络硬件,不太具备远程办公的基因,要补课的地方就额外多。

1. 按小团队划分并设定第一责任人

亚马逊CEO贝索斯提到过一个原则:如果两个披萨饼都喂不饱一个团队,那么这个团队可能就太大了。按照这个逻辑,我的团队可能只能容纳两个人。。。
玩笑开完了。但事实就是,对于一般人来说,能较好管理5~6个就已经是不错了。当团队人数超过这个规模,需要将团队拆分为6-10人的小团队规模,增加汇报层级,才能管得过来。
每个小团队可以包含前端、后端和测试,而数据库、UI等共享资源单独一个团队。

对每个小团队需要指定一个第一责任人(以下简称“责任人”)。这个责任人需要有以下的素质:

  • 对小团队成员知根知底
  • 快速响应的执行力和跟进能力
  • 对任务目标有充分的理解

2. 通知走大群,信息收集走小群

2.1 通知

远程办公期间的通知事项会比较多。邮件通知方式不能确保所有人都会在第一时间查看邮件。
通过即时通信的群通知,可以确保绝大部分人都能第一时间看到并响应。
即时通信大群的注意点:

  • 大群要求对于一般消息不要回复“收到”、“1”、“了解”等确认信息
  • 不得灌水,仅作通知用
  • 有疑问可以提。这样回复了一个人后,其他有同样问题的人也可以得到回复了

2.2 信息收集

当需要收集例如“疫情期间所在地”、“可以使用的远程办公方式”、“工作日报”等信息,还是通过小团队来分别收集效率较高。
如果只将问卷发在大群里,到了截止时间总会有那么一两个人没填。这就要靠负责人call电话等方式来催。

3. 小团队工作形式

分派工作也以小团队为粒度。
当责任人收到任务后,需要进行以下的日常管理工作:

  • 给出所承担工作的评估结果及具体计划
  • 制定具体开发人员的工作计划
  • 小团队内每日晨会,跟进进度,整理问题
  • 晚上下班前收取工作日报
  • 将小团队内部无法解决的问题向上反应到项目经理,并及时跟进进展

4. 每日晨会

网络会议的特点就是很容易发生两个人同时讲话,然后两个人注意到后又同时沉默。这会影响会议效率。所以每日晨会建议由一个人单独主持。内容包括:

  • 整体通知事项
  • 小团队各人当前的问题和风险
  • 前一天收集到的问题的反馈
  • 自由发言,收集当天新的问题

项目经理/责任人在会后私下和提出问题的人沟通,并在第二天晨会上公布问题进展。
晨会尽量固定时间,保持精简,尽量不超过15分钟。

5. 进度检视会/专题事项会

  • 主持人或项目经理提前1天发会议邀约
  • 大团队的管理者和项目经理需要制定项目整体跟踪事项表。事项表列出具体事项、责任团队、各阶段时间节点、进度百分比等
  • 每周1次或2次开大团队的进度检视会,小团队责任人参加。内容包含团队内同步各事项进度。着重解决需要团队间协同的事项
  • 会议中安排1个人记录会议纪要,并在会后发给所有与会人员

6. 协同

远程办公带来的最大挑战是协同。对协同最重要的是文档。
文档的形式可以是传统Word或Excel文档,也可以是API平台上的接口文档等。
文档的注意点主要有:

  • 工作内容尽量形成文档或表格,包括且不限于进度表、需求文档、详细设计、数据库设计、测试用例等
  • 文档放在svn等可以版本管理和便于查看的平台上
  • 尽量文档由专人负责编辑,以防提交时冲突
  • 如果必须由多人编辑的文档,可以考虑找在线系统方案替代。例如文档协同、API接口平台等

7. 到场

在限制到场人数的情况下,优先安排以下两类人员到现场:

  • 有管理职责
  • 自我驱动能力差

本文永久链接 [ https://galaxyyao.github.io/2020/03/02/管理-远程办公项目管理经验总结/ ]

中台-读《说透中台》和《企业IT架构转型之道》有感

春节期间看完了极客时间的《说透中台》的课程,顺便也读了《企业IT架构转型之道-阿里巴巴中台战略思想与架构实战》一书。这篇从实际项目的角度来想象一下,如果让我来负责公司的中台,应该怎么做。
首先说下评价:这个课程符合我对ThoughtWorks的刻板印象:有点滥用理论术语,干货不多;问题题了不少,解决方案不落地。可能是都是给其他公司做的项目,有保密协议的缘故。能理解,但还是不推荐。阿里的那本中台书更加实在。

1. 中台概念整理

1.1 中台的目的

中台的目的就是企业能力复用。

1.2 中台的分类

中台主流分为两大类:业务中台/数据中台。业务中台产生数据,数据中台做数据的二次加工,并将结果再服务于业务中台。
也有“技术中台”的概念,可以理解为一些技术中间件的整合和封装,但我倾向于不将其认定为中台。
中台强调一个复用。如果根本没有系统从零开始建设,一上来就搞中台很容易会过度设计。

2. 中台的抓手

中台会面对所有业务线的需求。虽然中台有企业级的属性,但不代表建设中台的时候必须梳理企业的全业务线。中台的愿景是能力复用,那么最好有具体的新业务作为抓手。

  • 阿里是以聚划算作为抓手
  • 蘑菇街的新增抓手是直播电商
  • 美菜网是遇到了生鲜和2B的一些新玩法需求

2.1 对当前所在公司构建业务中台的构想

《说透中台》中虚拟出来了一个极客地产。极客地产做中台的抓手是有新增的长租公寓的需求。长租公寓需要复用已有的地产投资和物业能力,但工程设计/招采/建设/装修/租赁等是从零开始。那么就可以针对地产投资和物业这两块来建设中台。而另外独立发展的业务线可以先跳过。
极客地产这个例子其实和当前所在公司挺像(为了避免再被信息安全“训诫”,这里就不提公司名字了)。已有的业务是投资,金融和运营三个板块,而现在新增了商业地产板块。是不是可以趁这个机会也建设中台?我觉得可以有针对性发展少数几个业务中台,主要做数据中台。
楼宇项目信息可能是少数比较共通的部分,但投前和投后在剩余的部分的交集有限。没有复用,还不如将功能独立建设在自己各自板块的应用里。当然现在对其他三个板块了解也有限,可能在了解更多后会推翻这个判断。

3. 中心与服务如何划分

在具体动手开始搭建中台后,我们首先面临的问题是服务中心怎么划分怎么建设。
淘宝的经历是首先四个服务中心:

  • 用户中心
  • 商品中心
  • 交易中心
  • 店铺中心

用户中心因为调用频繁收效大,而且复杂性和重要性较小,不出意外成为了最开始的试点。商品中心后来又拆出了评价中心。交易中心拆出了营销中心。另外还增加了一个库存中心。
中心的划分的原则是:“高内聚、低耦合”。如果只有增删改查这样的简单需求,不建议单独拆出一个中心。像是积分这种,等发展到足够丰富或对其他业务中心的影响已经不可忽视再拆。
每个中心也可以由多个服务组成。例如交易中心可以分为订单服务和购物车服务。服务中心是业务领域的概念,是为了业务和数据的完整性而设立的。而包含的子服务模块是根据系统架构设计的层面来考虑的。但一开始不要拆得太细。

4. 中台的其他技术考量点

4.1 服务插件

京东服务技术中台探索与实践》的视频中提到了他们建设中台的时候有一个“服务插件”的概念。简单来说就是对于非常个性化的需求(例如对用户体验的预警),提供插件协议,由相应前台团队自己来开发插件。

服务插件1

这种中台的场景可能更适合中台把前端页面也包揽的情况。下图是插件在实际页面上的规定展示区域。

服务插件2

4.2 租户

还是《京东服务技术中台探索与实践》的视频。为了防止某些用户在大促的时候把中台资源(包括计算资源和存储资源)给吃完,引入了租户的概念。
租户的设计对于有2C秒杀大促等场景的企业可能是必要的,毕竟不能在中台层面产生雪崩。

租户

4.3 配置化

包括蘑菇街和京东服务中台的例子中,都提到了中台可以成为业务逻辑的下沉,前台应用做配置。
像蘑菇街就是将促销的逻辑做成模板:

配置化

5. 中台的实施路径

阿里的中台实施路径分为三个阶段:

  • API as Service
  • Product as Service
  • Solution as Service

第一个阶段比较好实现,第三个阶段是到一定层次后的追求,我觉得关键是第二个阶段,就是将中台打造成一个产品。
打造成产品,意味着将API进行场景化的组装。如果只是一堆API列表,无法达成快速支持前台的目的。
除此以外,产品化也意味着:

  • 产品有自己的定位,可以选择性地实现前台需求
  • 产品要想方设法为客户(前台)体现价值,保持用户满意度才能生存下来。如果一定期限内无法获取前台用户,或前台用户不满意,则及时中止建设止损
  • 产品要讲究易用性,提供完善的文档和手册

中台的绩效考核可以参考阿里的做法:

  • 服务稳定40%
  • 业务创新25%(适当允许因此带来的上线事故)
  • 服务接入量20%
  • 客户满意度15%

本文永久链接 [ https://galaxyyao.github.io/2020/02/05/中台-读说透中台和企业IT架构转型之道有感/ ]

容器-14-国内Windows10环境安装Minikube

上家公司虽然有这样那样的问题,但在能让我掌控的服务器资源自由度上,也不是随便在哪家公司就能有的。能随便申请个半打一打的4核8G的虚机来搞事情什么的。。。跳槽后就只有自己的Windows工作机了。Docker Desktop搞了半天也没法启用Kubernetes,这也是为什么之前的“Kubernetes实战”系列到7月就戛然而止的原因。
只靠Docker Desktop,平时开发的时候起个数据库或redis是足够用了,但像service mesh之类的就玩不了了。趁年前有空,搭了一套Minikube,把步骤顺便记录一下。原本想合并到之前kubeadm安装的那篇里,但可能会翻起来不方便,还是单独另开一篇吧。
后续“Kubernetes实战”系列都会基于minikube环境来搭建。

1. 软硬件条件

现在内存也不值钱了,插个16G足够玩了。
操作系统上,虽然Windows 10家庭版+VirtualBox/VMWare也可以,但从硬件利用率角度,还是用Windows 10企业版/专业版/教育版+Hyper-V比较好。
在控制面板->程序->启动或关闭Windows 功能 里面打开所有Hyper-V选项然后重启。
重启后运行systeminfo,看到如下内容,说明操作系统层面已经ok了:

1
Hyper-V 要求:     已检测到虚拟机监控程序。将不显示 Hyper-V 所需的功能。

Docker Desktop是否安装不影响,但在安装Minikube的过程中最好不要启动。在安装过程中报过一个create: precreate: no External vswitch nor Default Switch found的报错,不确定是不是相关。
顺带提一句,如果装了Docker Desktop,可以在Settings->Daemon->Registry mirrors里填写:https://dockerhub.azk8s.cnhttp://hub-mirror.c.163.comhttps://docker.mirrors.ustc.edu.cn
另外感谢这篇docker/kubernetes国内源/镜像源解决方式 - xinkun的博客 | Xinkun Blog的整理,我也复制一下备忘:

global proxy in China format example
dockerhub (docker.io) dockerhub.azk8s.cn dockerhub.azk8s.cn/<repo-name>/<image-name>:<version> dockerhub.azk8s.cn/microsoft/azure-cli:2.0.61 dockerhub.azk8s.cn/library/nginx:1.15
gcr.io gcr.azk8s.cn gcr.azk8s.cn/<repo-name>/<image-name>:<version> gcr.azk8s.cn/google_containers/hyperkube-amd64:v1.13.5
quay.io quay.azk8s.cn quay.azk8s.cn/<repo-name>/<image-name>:<version> quay.azk8s.cn/deis/go-dev:v1.10.0

2. 网络条件

以防万一请先关闭Windows防火墙。
因为你懂的那个原因,需要本地搞个SS的梯子。如果哪个步骤因为网络原因卡住了,可以切成代理再试一次。

3. 安装步骤

3.1 安装Minikube

首先注意一下,后续的步骤都需要用管理员权限的命令行来执行。Powershell应该也行。
安装步骤在官方文档里有详细描述,但似乎遗漏了需要先安装Kubernetes Cli的提示。
所以最彻底的方法是通过Chocolatey来安装,它会帮忙把该装的都装好:

1
choco install minikube

3.2 Minikube初始启动

接下来就可以通过start命令来启动了。第一次启动可能会因为kubeadm和kubelet镜像下载失败而失败。不过不用担心,可以反复执行的,大不了用minikube delete重置重来。第一次启动主要是看个版本号。

1
minikube start --vm-driver=hyperv --cpus=2 --memory=6g --image-repository="registry.cn-hangzhou.aliyuncs.com/google_containers"

PS1. memory参数建议在start的时候就加上。通过minikube config set memory 4096来调整,需要delete了重新start。
PS2. 有些步骤里会加上一个--registry-mirror=https://registry.docker-cn.com的参数。但实际Docker的国内站已经挂了(公司都快撑不下去了,有点惨),加个image-repository的参数就足够了。这里image-repository用的是阿里的镜像。
PS3. 如果怀疑是网络问题想通过代理下载,可以添加HTTP_PROXY等参数,例如:

1
minikube start --vm-driver=hyperv --cpus=2 --memory=6g --image-repository="registry.cn-hangzhou.aliyuncs.com/google_containers" --docker-env HTTP_PROXY=http://127.0.0.1:1080 --docker-env HTTPS_PROXY=http://127.0.0.1:1080 --docker-env NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,192.168.99.0/24,192.168.39.0/24

3.3 手动下载kubeadm和kubelet

第一次启动可能会非常花时间(视你的网络而定)。如果失败的话,可以根据kubeadm和kubelet下载失败报错信息,来确定k8s的版本。我安装的minikube使用的版本是v1.17.0。
然后可以手动从googleapis网站下载(Windows机器默认没有curl,也可以通过Chocolatey安装,参考这篇

1
2
curl -Lo kubeadm http://storage.googleapis.com/kubernetes-release/release/v1.17.0/bin/linux/amd64/kubeadm
curl -Lo kubelet http://storage.googleapis.com/kubernetes-release/release/v1.17.0/bin/linux/amd64/kubelet

下载后扔到C:\Users\用户名\.minikube\cache\版本号的目录里。
如果其他K8S的组件下载失败,也可以使用类似的方式下载后扔到C:\Users\用户名\.minikube\cache\images\registry.cn-hangzhou.aliyuncs.com\google_containers的目录里。例如kube-proxy:

1
curl -Lo kube-proxy_v1.17.0 http://storage.googleapis.com/kubernetes-release/release/v1.17.0/bin/linux/amd64/kube-proxy

但这招对版本号不一样的例如etcd没用。这个我是靠重试多次硬抗的。
对于国内网络来说,这个步骤很花时间,不要急慢慢等。

3.4 更新minikube kubectl

如果同时装了Docker Desktop和minkube,会提示Docker下的kubectl.exe的版本不够高。

1
2
* 完成!kubectl 已经配置至 "minikube"
! C:\Program Files\Docker\Docker\Resources\bin\kubectl.exe is version 1.14.0, and is incompatible with Kubernetes 1.17.0. You will need to update C:\Program Files\Docker\Docker\Resources\bin\kubectl.exe or use 'minikube kubectl' to connect with this cluster

接下来先用minikube stop停止,然后用minikube kubectl options的命令,会提示下载最新版本的kubectl:

1
2
$ minikube kubectl options
* 正在下载 kubectl.exe v1.17.0

最新版的kubectl会下载到C:\Users\用户\.minikube\cache\v版本的目录里,然后复制到Docker的bin目录(例如C:\Program Files\Docker\Docker\Resources\bin\)里覆盖即可。

4. 验证

安装好后就可以验证了。先看看是不是所有系统容器都启动了:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl get pod -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-7f9c544f75-j6cfj 1/1 Running 1 6h10m
kube-system coredns-7f9c544f75-wpcn7 1/1 Running 1 6h10m
kube-system etcd-minikube 1/1 Running 1 6h10m
kube-system kube-addon-manager-minikube 1/1 Running 1 6h10m
kube-system kube-apiserver-minikube 1/1 Running 1 6h10m
kube-system kube-controller-manager-minikube 1/1 Running 2 6h10m
kube-system kube-proxy-kbnsr 1/1 Running 1 6h10m
kube-system kube-scheduler-minikube 1/1 Running 1 6h10m
kube-system storage-provisioner 1/1 Running 1 6h10m
kubernetes-dashboard dashboard-metrics-scraper-7b64584c5c-6k8f8 1/1 Running 1 6h4m
kubernetes-dashboard kubernetes-dashboard-79d9cd965-565j6 1/1 Running 2 6h4m

然后可以看看dashboard。本机就是好不用token:

1
minikube dashboard

然后也可以照官方的Example里的,创建个echo server来验证。需要注意image里的k8s.gcr.io需要替换为registry.cn-hangzhou.aliyuncs.com/google_containers

1
2
3
kubectl create deployment hello-minikube --image=registry.cn-hangzhou.aliyuncs.com/google_containers/echoserver:1.4
kubectl expose deployment hello-minikube --type=NodePort --port=8080
minikube service hello-minikube

最后一句minikube service <service名>的作用是获取本地集群中指定服务的kubernetes URL。对于以NodePort对外暴露的服务,会自动打开浏览器并跳转到对应的ip+端口。注意对应的ip是Hyper-V管理器中的“vEthernet (Default Switch)”。

5. 启用ingress

minikube启用ingress还是挺简单的,可以参考Installation Guide - NGINX Ingress Controller,一句命令启动:

1
minikube addons enable ingress

但有可能会遇到Back-off pulling image的报错信息。看到image的名字里包含<quay.io>就可以知道又是被墙拦住了。对于类似的问题,可以采用以下的步骤,从Azure拉取后打tag。对于ingress,命令如下:

1
2
3
minikube ssh
docker pull quay.azk8s.cn/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1
docker tag quay.azk8s.cn/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1 quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1

部署完成后可以通过kubectl get pod -n kube-system的命令来确认ingress是否有安装成功。如果失败的话,最坏结果有可能需要minikube delete后重新start。

6. 常见问题

6.1 minikube delete失败

遇到过一次minikube delete失败,报的是C:\Users\用户名.minikube\machines下某个conf文件找不到。尝试手动删除该目录会报文件被锁定,无法删除。重启后文件夹依然被锁定。
这个时候打开services.msc,停止“Hyper-V 虚拟机管理”服务后,就可以删除了。删除完把这个服务重新启用即可。

6.2 Unable to connect to the server

在kubectl apply命令的时候有些时候会出现Unable to connect to the server的报错。这种情况下kubectl get node的命令也会失败。电脑重启后问题解决。原因暂不明。。。可能是minikube的bug。

7. 参考资料

常用操作可以参考这个官方文档:
使用 Minikube 安装 Kubernetes - Kubernetes

本文永久链接 [ https://galaxyyao.github.io/2020/01/22/容器-14-国内Windows10环境安装Minikube/ ]

Java-从FeignClient的Ambiguous mapping报错,重温RequestMapping原理

1. 微服务的公共API模块

微服务之间调用进程会出现DTO实体类的重复定义。比如服务A的接口返回User实体,服务B接收的时候,也需要定义一个同样的User实体。
在引入了Feign后,就有了一个避免项目间重复定义实体类的简单方案:我们可以在服务A开发的时候专门抽出来一个API模块。

API公共模块

这个API模块可以包含接口方法定义,URI以及和对外实体类定义(DTO),可以认为是A和B之间互通的约定。
一个最简单的API模块代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DemoDto implements Serializable {
private String text;
}

@RequestMapping("/demo")
public interface DemoApiService {
@GetMapping("/hello")
DemoDto hello();
}

服务A的Controller负责对接口定义进行实现:

1
2
3
4
5
6
7
@RestController
public class DemoProducerController implements DemoApiService {
@Override
public DemoDto hello() {
return new DemoDto("hello");
}
}

服务A项目将API模块发布到Maven私服上。服务B项目只需要对API模块添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.galaxy.demo</groupId>
<artifactId>feign-demo-api</artifactId>
<version>1.0.0</version>
</dependency>

并且扩展一下该接口并添加@FeignClient注解:

1
2
3
@FeignClient(name = "demo", contextId = "demoSpiService", url = "http://localhost:8080/")
public interface DemoSpiService extends DemoApiService {
}

就可以很轻松地像调用本地方法一样调用A应用的接口了。

1
2
3
4
5
6
7
@Resource
private DemoSpiService demoSpiService;

@GetMapping("/hello")
public String hello() {
return demoSpiService.hello().toString();
}

2. Ambiguous mapping报错

如果你像我上面描述的那样实现,就会在消费者服务B启动的时候遇到如下的报错信息:

1
2
3
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'com.galaxy.demo.feign.consumer.spi.DemoSpiService' method 
com.galaxy.demo.feign.consumer.spi.DemoSpiService#hello()
to {GET /demo/hello}: There is already 'demoConsumerController' bean method

报错信息很直白:同一个URI被重复映射了两次。一次是在DemoConsumerController,一次是在DemoSpiService。
But Why? DemoSpiService里只是一个FeignClient,不是RestController啊?

3. @RestController,@Controller,@RequestMapping原理重温

我们通过这个问题,正好来重温一下@RestController,@Controller和@RequestMapping几个Spring中的经典概念。

3.1 @RestController

我们先来看一下@RestController的原代码:

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}

可以看到@RestController=@Controller+@ResponseBody

Spring MVC流
上图是一个Spring MVC从接收请求到返回响应的完整流程。我理解对于SpringBoot的RestController来说,在第四步没有返回ModelAndView,而是直接返回了Json,并通过@ResponseBody将Json直接写到了响应Body,略过了第5步和第6步。

3.2 @Controller和@RequestMapping

如果只从@Controller的源代码来看,@Controller只是@Component的一个别名。

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

但注解怎么用不是看定义的。从Spring的AbstractHandlerMethodMapping.java的源代码,我们可以看到Spring会根据一个名为isHandler方法的判断结果,对Handler处理器里的方法进行扫描,获得URL映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
		if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
// 省略部分
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);

而isHandler的逻辑很简单,就是看Bean上是否有@Controller注解或@RequestMapping注解。参见源代码

1
2
3
4
5
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

4. Ambiguous mapping报错原因总结和解决方案

归根到底,Ambiguous mapping报错原因在于上面的那个逻辑中使用的是“或”(||),而不是“和”(&&)。
由于我们的DemoSpiService扩展了DemoApiService,而DemoApiService的接口定义上有@RequestMapping注解,于是DemoSpiService也被Spring MVC扫描Handler了。而偏生对于DemoSpiService和DemoConsumerController的URL路径都是“/demo”,于是就产生了冲突。
知道了原因后,解决方案也就很简单了:修改一下DemoConsumerController的@RequestMapping的URL,例如改为@RequestMapping("/consumer/demo"),就可以成功启动了。

你可能会担心@FeignClient+API模块是否会暴露不该暴露的接口?直接访问的话会返回404:

1
2
3
4
5
6
7
{
"timestamp": "2019-12-23T12:51:05.376+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/demo/hello"
}

也很容易理解:请求在找RequestMapping对应的View:”/demo/hello”。但View不存在,就只能返回404了。只有在DemoSpiService上主动添加@ResponseBody注解,才能对外暴露。

5. 参考资料

这篇是主要参考资料。作者认为这是Spring MVC的锅。我理解指的是“或”的那个逻辑。但我觉得当初Spring这么写肯定是有原因的。。。虽然我没找到相关文章。
FeignClient 出现 Ambiguous mapping 重复映射 | Japari Park

另外是两篇Spring原理解析参考
SpringMVC在@RequestMapping配置两个相同路径 - Text_Dexter - 博客园

Spring MVC — @RequestMapping原理讲解-1 - 小小默:进无止境

本文永久链接 [ https://galaxyyao.github.io/2019/12/23/Java-从FeignClient的Ambiguous-mapping报错-重温RequestMapping原理/ ]

Java-Feign+服务注册的多环境方案

微服务的开发模式下,联调和服务注册一旦涉及多个环境(开发/SIT/UAT),就会变得有些复杂。本文总结一下我们在此问题上尝试过的几个workaround,以及最终推荐的方案。

1. 背景

以下描述的案例中,将我们所拥有的服务精简为三个:

  • um:用户微服务
  • ent:企业微服务
  • bi:BI微服务
    ent会调用um;bi会调用ent和um。
    网络环境分成办公网段和开发环境网段。办公网段可以访问开发环境网段,但开发环境网段无法访问办公网段。
    三个微服务都被打包成镜像,以单副本Pod的形式部署在K8S云的开发环境节点上。
    服务注册使用Nacos,网关路由使用的是Zuul。

部署环境

2. 单环境内部请求流程

如果只考虑SIT环境,整个服务注册+请求的处理流程可以简单描述如下:

  1. um-sit服务(um的sit环境,下同)启动,将自己的service ip注册到Nacos服务端
  2. ent-sit服务启动,将自己的service ip注册到Nacos服务端
  3. 前端web对http://域名/api/ent-sit 的某个接口发起请求
  4. 通过K8S Ingress的域名映射,找到了Zuul应用
  5. Zuul向Nacos查询ent-sit的地址,得到ip:172.0.0.2。这个是ent-sit的service内部ip
  6. Zuul将请求转给ent-sit的service,Pod里的ent-sit容器中的应用接收到请求,开始处理
  7. ent-sit容器在处理过程中需要解析token,于是向Zuul请求um-sit
  8. Zuul向Nacos查询um-sit的地址,得到ip:172.0.0.1。这个是um-sit的service内部ip
  9. Zuul将请求转给um-sit。um处理完token,返回用户信息
  10. ent-sit处理结束,将结果返回给Zuul
  11. Zuul将结果转给前端web,流程结束

单环境内部请求流程

3. 遇到的问题

联调和测试过程中我们遇到了两个主要问题:

  • 联调会串服务
  • 无法和测试环境的服务联通

3.1 串服务

假设Alice和Bob都在开发ent,服务名都是ent-dev。于是Nacos记录了两个服务注册信息。
Cathy想和Alice联调。但如果Cathy配置调用的服务id也是ent-dev,请求就有一定几率会飘到Bob那里。那么很可能会发生Cathy的请求返回的结果不稳定,时对时错。

一种解决方案就是每个人在本地将自己的spring.application.name改为“服务名-姓名”,例如:ent-alice。

服务注册-workaround-1

但这个方案也存在问题:很容易在提交代码的时候误提交了自己的个人配置。Git里这个配置文件修改频繁,一看log就是服务名从Alice改为Bob,然后又被改会Alice。
如果只是这个问题,还有办法可以搞定,例如将服务名放到环境变量等。下一个问题是真正具有阻碍性的。

无法和测试环境的服务联通

在办公网段开发过程中,会发现无法调通SIT的微服务。
究其原因,这个是由于办公网段无法访问到service内部ip导致的。

服务注册-问题

办公网段ent-dev尝试调用um-sit服务的流程如下:

  • um-sit服务启动,将自己的service ip(172.0.0.1)注册到Nacos服务端
  • 本地的ent-dev启动,将自己的ip(10.0.0.1)注册到Nacos服务端
  • ent-dev通过name(um-sit)发起请求,向Nacos服务端查询um-sit的地址
  • Nacos服务端返回172.0.0.1
  • ent-dev尝试请求172.0.0.1,但由于这个地址是K8S的内部地址,外部无法访问,所以请求失败

一个解决方案就是每个人自己本地也起一个um的服务,假设服务名为um-alice。然后将请求的服务名也改为同样的服务名。
这样当发起请求时,Nacos会返回本机的地址,自然请求就可以成功了。
但这个解决方案除了和上个解决方案有同样的问题(容易误提交自己的个人配置)之外,也会导致每个人开发过程中都需要启动一堆依赖的微服务。姑且不说开发机的性能压力,也容易因为没有及时更新依赖服务的代码,导致联调出错。

4. 解决方案

4.1 解决方案一:在容器之外再部署一套微服务

既然service的内部ip地址无法被办公网段访问,那么另外以非容器方式在ECS上另外部署一套dev环境,就可以解决网络访问的问题。
但这个解决方案不完美:

  • 没有解决个人配置的问题
  • 在基于容器的持续集成方案之外,多维护了一套持续集成方案
  • 需要多部署一套环境,消耗硬件资源

4.2 【推荐】解决方案二:不使用name方式访问,使用域名/ip方式

需要同时避免串服务和个人配置这两个看起来互相冲突的问题,看起来只有放弃通过服务name方式调用,改为url调用。
通过Feign可以简化调用的代码。只要在@FeignClient的参数里配置了url,就会优先使用url。范例如下:

1
2
3
4
5
6
7
8
9
@FeignClient(name = "jsonPlaceHolderClient", url = "${feign-client.json-place-holder.url}"
, contextId = "JsonPlaceHolderClient")
public interface JsonPlaceHolderClient {
@GetMapping(value = "/posts")
List<Post> getPosts();

@GetMapping(value = "/posts/{postId}")
Post getPostById(@PathVariable("postId") Long postId);
}

application-dev.yml配置文件中,feign-client.json-place-holder.url可以默认填写为sit测试环境的地址。这样如果只是作为基础服务来调用(例如用户服务),就不需要在本地启动了。同样以办公网段ent-dev调用um-sit服务的流程作为范例:

  • um-sit服务启动,将自己的service ip(172.0.0.1)注册到Nacos服务端
  • 本地开发机的ent-dev启动
  • ent-dev通过域名http://域名/api/um-sit,对um的某个接口发起请求
  • Zuul收到请求,向Nacos查询um-sit的地址,得到ip:172.0.0.1。这个是um-sit的service内部ip
  • Zuul将请求转给um-sit的service,Pod里的um-sit容器中的应用接收到请求,开始处理
  • um-sit容器中的应用处理请求完毕,返回结果给Zuul
  • Zuul将结果转给本地开发机
    和K8S的Service不同,Ingress是可以被容器外访问到的,所以网络连通性上也没有任何问题。

如果是需要本机服务联调或与其他开发进行联调,只需要将url改为localhost或其他开发的ip即可。这样就等同于不涉及服务注册的直连。
提交的时候把这个临时改动revert回来,就不会将个人配置提交到代码仓库了。

5. Feign配置中的name和url

在最新版的Spring Cloud OpenFeign中,@FeignClientname属性是必需的。参见Spring Cloud OpenFeign

1
Previously, using the url attribute, did not require the name attribute. Using name is now required.

上文提到了如果同时配置了name和url,会优先使用url,而不是通过name访问服务。原理我们可以通过源代码来说明。这段是FeignClientFactoryBean的源代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context
* information
*/
<T> T getTarget() {
//FeignContext在FeignAutoConfiguration中自动注册,FeignContext用于客户端配置类独立注册
FeignContext context = this.applicationContext.getBean(FeignContext.class);
//创建Feign.Builder
Feign.Builder builder = feign(context);

//如果@FeignClient注解没有设置url参数
if (!StringUtils.hasText(this.url)) {
//url为@FeignClient注解的name参数
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
//加上path
this.url += cleanPath();
//返回loadBalance客户端,也就是ribbon+eureka/Nacos的客户端
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//@FeignClient设置了url参数,不走服务注册的负载均衡
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
//加上path
String url = this.url + cleanPath();
//从FeignContext中获取client
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
builder.client(client);
}
//从FeignContext中获取Targeter
Targeter targeter = get(context, Targeter.class);
//生成客户端代理
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}

从代码可以看到,只有没有设置url的情况下,才会通过loadBalance方法生成Ribbon的动态代理。
更多关于Spring Cloud OpenFeign的源代码分析,可以参见本文最后的参考资料。

6. 一些想法

实际调用过程中会发现第一次通过域名调用会较慢(2-3秒),但第二次就很快了。这是由于Zuul会通过SpringMVC对请求进行缓存。
但其实Zuul的路由功能Ingress本身已经实现得很好了。多引入一个Zuul会增加运维架构的复杂度,也会带来潜在的性能瓶颈。不过这个目前不在我们的控制范围。。。Zuul除了路由之外也可以做一些通用的token校验等,也并不是完全冗余,只是我们目前没有这么使用。

7. 参考资料

spring-cloud-openfeign原理分析 | 拍拍贷基础框架团队博客

本文永久链接 [ https://galaxyyao.github.io/2019/12/09/Java-Feign-服务注册的多环境方案/ ]

数据库-转到PostgreSQL的新手Tips

从Oracle或MySQL切换到PostgreSQL(以下简称pgsql)后,多少有些不一样的地方需要适应。这里就将和开发相关的一些区别挂一漏万地列举一下。

1. Schema模式

和Oracle与MySQL一样,pgsql中也有TableSpace(表空间),用于定义用来存放表示数据库对象的文件的位置。
但在Schema(模式)的定义上,三者有很大的差别。
对于MySQL,模式与数据库同义。甚至可以用CREATE SCHEMA来创建数据库,效果和CREATE DATABASE一样。
对于Oracle,schema与数据库用户密切相关:

A schema is a collection of logical structures of data, or schema objects. A schema is owned by a database user and has the same name as that user. Each user owns a single schema.

而pgsql中,层次结果如下:
PostgreSQL Hierachy

从图中可以看到,schema是database与table中间的一层。可以理解为命名空间类似的概念。当新创建一个数据库时,pgsql会默认创建一个public schema。如果没有指定的话,就是以public schema来操作各种数据对象。 例如:CREATE TABLE products ( ... ) 等同于 CREATE TABLE public.products ( ... )
schema不能互相嵌套。同一个schema下不能有重复的对象名字,但在不同schema下可以重复。
schema与database的差别在于schema不是严格分离的:一个用户可以访问他所连接的数据库中的任意模式中的对象。

对于数据库管理人员来说,还需要了解一下授权相关的差别,但在本文中就略过了。更多可以参考这篇:PostgreSQL · 特性分析 · 逻辑结构和权限体系

1.1 Schema与开发相关

连接url字符串中除了需要指定数据库之外,还需要加一个currentSchema。例如下面的范例中,database是pabem,schema是pabem_um_dev

1
jdbc:postgresql://localhost:5432/pabem?currentSchema=pabem_um_dev

schema可能对需要跨数据源的应用开发带来一些简便。如果需要跨的两个数据源只是同一个数据库的两个schema,就可以去掉连接url中的currentSchema,就可以当单数据源应用来开发了。

参考资料
表空间

默认schema是public,切换执行sql所在schema的语法是:

1
set search_path to <schema_name>

2. 自增字段

和MySQL中使用的auto increment不同,PostgreSQL和Oracle类似,都是用sequence(序列)。
sequence的好处在于可以让多张表共享同一个自增序列,但创建起来的确也挺麻烦。所以pgsql还新增了一个语法糖serial。
和数值类型一样,也分为smallserial, serial和bigserial:

Name Storage Size Range
smallserial 2 bytes 1 to 32767
serial 4 bytes 1 to 2147483647
bigserial 8 bytes 1 to 9223372036854775807

2.1 Serial与开发相关

对于自增的id字段,需要在Entity的属性上加上注解:

1
@GeneratedValue(strategy = GenerationType.IDENTITY)

3. 数据类型映射

PostgreSQL数据类型 Oracle数据类型 MySQL数据类型 备注
char char char Oracle中char(n)的n表示byte数,而pgsql和mysql中表示字符数。对于中文字无需除以2或除以3
varchar varchar2 varchar 同char
text clob text
bytea blob blob
smallint number(4) smallint,tinyint pgsql中没有tinyint,所以我们的布尔型字段用smallint类型
int number(9) int int是integer的缩写
bigint number(18) bigint
decimal decimal decimal decimal与numeric等价,都是SQL标准。我们就统一用decimal
date (无) date Oracle没有纯日期类型,date会返回日期和时间
timestamp timestamp timestamp pgsql还有timestampz表示带时区的时间戳

参考资料
PostgreSQL: Documentation: 11: 8.1. Numeric Types

4. 常用函数与语法差异

4.1 DUAL

pgsql中的select可以省略from,所以不再需要强制加一个from dual

4.2 日期和时间

  • 当前时间:now()
  • 日期转字符串:select to_char(current_date,'YYYY-MM-dd');
  • 时间转字符串:select to_char(now(),'YYYY-MM-dd HH24:MI:SS');

4.3 字符串

  • 拼接:select 'a'||'b' as col1;
  • 获取指定字符串的下标:select position('om' in 'Thomas');

4.4 序列

获取序列下一个值的语法为:nextval('sequence_name')

4.5 行数

1
2
3
4
ROW_NUMBER() OVER(
[PARTITION BY column_1, column_2,…]
[ORDER BY column_3,column_4,…]
)

例如:

1
2
3
4
5
6
7
8
9
10
11
SELECT
product_id,
product_name,
group_id,
ROW_NUMBER () OVER (
PARTITION BY group_id
ORDER BY
product_name
)
FROM
products;

4.6 NVL(判断为空赋值)

1
SELECT coalesce(null,0) as col1;

4.7 分页

PostgreSQL中的分页语法和MySQL类似:

1
2
3
4
5
SELECT
*
FROM
table
LIMIT n OFFSET m;

4.8 CRUD语法差异

网上也看到有人整理了一下CRUD语法的差异:
PostgreSQL MySQL Gramma Difference
简单总结一下,就是支持插入/更新/删除并返回,以及插入冲突则更新或什么不做。前者从通用性考虑不推荐,后者MyBatis Plus也封装了一个,不一定需要使用数据库的实现。
表关联多字段更新倒可能比较常用,在Oracle中也有同样的语法:

1
2
3
update table1 set (col1, col2) =
(select col1, col2 from table2
where table2.col3 = table1.col3)

参考资料
MySQL和PostgreSQL的常用语法差异-云栖社区-阿里云

5. Rule规则系统

这个是pgsql中的一个特性。或者更准确地说,是查询重写规则系统,即把根据既定规则修改后的查询再提交给查询规划器。
实际上PostgreSQL中的视图就是通过规则系统来实现的。例如如下的查询:

1
CREATE VIEW myview AS SELECT * FROM mytab;

内部的规则:

1
2
3
CREATE TABLE myview (same column list as mytab);
CREATE RULE "_RETURN" AS ON SELECT TO myview DO INSTEAD
SELECT * FROM mytab;

pgsql中同样也有触发器。你可能会发现规则系统和触发器的作用有点相像。其实他们的作用域有重叠的部分,也有另一方无法替换的场景。
只有触发器能做的场景:约束触发器。触发器能抛出异常,而规则系统只能静默地选择处理或不处理。而只有规则系统能做更新视图。
另外触发器会对被影响的每一行触发一次,而规则系统是一次性的重写。所以在某些场景下规则系统的性能会高于触发器。

参考资料
Chapter 41. 规则系统
PostgreSQL: Documentation: 11: 41.7. Rules Versus Triggers
PostgreSQL的规则系统 | P.Linux Laboratory

6. Java开发配置

使用JPA作为数据源的时候,启动的时候会告警:

1
2
3
Caused by: java.sql.SQLFeatureNotSupportedException: 这个 org.postgresql.jdbc.PgConnection.createClob() 方法尚未被实作。
at org.postgresql.Driver.notImplemented(Driver.java:692) ~[postgresql-42.2.8.jar:42.2.8]
at org.postgresql.jdbc.PgConnection.createClob(PgConnection.java:1268) ~[postgresql-42.2.8.jar:42.2.8]

这是由于Hibernate尝试验证PostgreSQL的CLOB特性,但是PostgreSQL的JDBC驱动并没有实现这个特性,所以抛出了异常。
可以增加配置,关闭这个特性的检测:

1
2
3
4
5
6
spring:
jpa:
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false

参考资料:SpringBoot连接PostgreSQL - ldp.im - 博客园

如果只使用MyBatis就不需要加这个配置了。

7. 排序规则与大小写敏感

大小写敏感分为两个不同的方面:数据库对象名的大小写敏感,以及字段内容的大小写敏感。

7.1 数据库对象名的大小写敏感

PostgreSQL在创建数据库对象(表/字段等)时,会默认将对象名改为小写。
例如会将如下的SQL

1
SELECT FullName FROM Person

转换为

1
SELECT fullname FROM person

如果一定要使用大小写敏感的对象名,则需要在创建和查询的时候都带上双引号。例如:

1
CREATE TABLE "Person" ("FullName" VARCHAR(100), "Address" VARCHAR(100))

但非常不推荐这种方式。

7.2 字段内容的大小写敏感

PostgreSQL查询的时候是大小写敏感的。而且在建库时不能像MySQL那样,通过collation参数来指定数据库是否大小写敏感。
如果需要进行大小写不敏感的查询和模糊查询,可以使用如下两种方法之一:

  • 等号=LIKE的两边的表达式加上LOWER()UPPER()
  • 使用ILIKE(应该是Insensitive Like的缩写吧)

例如:

1
select * from person where lower(user_name) like lower('%alice%')

1
select * from person where user_name ilike '%alice%'

LIKEILIKE也可以换成~~~~*。但为了SQL的可读性和统一,还是避免使用这样的语法吧。
对于text类型的字段,可以在PostgreSQL安装citext模块后,改为citext类型。这样就可以大小写不敏感了。

7.3 字段内容的大小写敏感带来的问题

字段内容大小写敏感可能会带来三个问题:

  • 排序
  • 性能
  • 索引

先来看排序。 因为大小写敏感,所以英文是按照ASCII排序。’a’开头的内容会被排在’B’之后。所以如果需要忽略大小写来排序,则排序字段也需要加lower

1
select * from person order by lower(user_name)

性能方面,有人做过测试,使用lower+like会比ilike快17%左右。再考虑到数据库迁移过程中的兼容性,还是推荐使用lower+like
通过UNIQUE或者PRIMARY KEY隐式产生的索引是大小写敏感的。如果使用lower的话,就不会走索引。如果对这方面有性能要求的话,可以给PostgreSQL安装上pg_trgm模块。

8. 其他MySQL与PostgreSQL比较

  • PostgreSQL中天然支持emoji,不需要像MySQL中一样专门设置utf8mb4编码
  • PostgreSQL和Oracle一样有物化视图
  • 支持CTE语法
  • 支持intersect语法
  • PostgreSQL中没有单独的存储过程,是通过Function实现的

9. 其他Oracle与PostgreSQL比较

  • NULL与空字符串在Oracle里是同一含义,但在pgsql中是不同的
  • 同义词synonym在pg中使用search_path来实现,例如:SET search_path TO myschema;

10. PostgreSQL独有特性

json/jsonb

这两个是PostgreSQL专有的数据类型。从用户操作的角度来说没有区别,区别主要是存储和读取的系统处理(预处理)和耗时方面有区别。json写入快,读取慢,jsonb写入慢,读取快。
有文章说jsonb的性能已经优于MongoDB的BSON。但至少有一个好处是如果需要处理json数据,在有PostgreSQL的情况下可以少引入一个数据库。

GIS

PostGIS基本成为了空间地理信息数据的存储标准。

11. 其他

如果想在本机Docker Desktop上启动pgsql,用官方的postgres:latest好像会有些问题,需要改为用alpine镜像。命令可参考:

1
docker run --name posttest -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:alpine

本文永久链接 [ https://galaxyyao.github.io/2019/09/22/数据库-转到PostgreSQL的新手/ ]

Maven-组织内部项目统一配置DistributionManagement

搜了一下中文技术博客上似乎没有相关的文章,就简要翻译一下。

需求

假设公司内部有非常多Maven项目,需要deploy到一个内部maven私有仓库中。
如果希望maven deploy命令可以成功执行,一般需要在pom.xml中添加:

1
2
3
4
5
6
<distributionManagement>
<repository>
<id>nexus-site</id>
<url>http://central_nexus/server</url>
</repository>
</distributionManagement>

但需要deploy的项目很多的情况下,我们肯定不希望在每个项目的pom文件中都重复添加这个配置。

方案一

为所有项目增加一个公共的parent pom项目。那么只需要在这个项目的pom文件中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>your.company</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>

<distributionManagement>
<repository>
<id>nexus-site</id>
<url>http://central_nexus/server</url>
</repository>
</distributionManagement>

</project>

然后使其他项目的parent项目变成这个项目:

1
2
3
4
5
<parent>
<groupId>your.company</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0</version>
</parent>

方案二

方案一存在两个问题:

  • 如果代码泄露或将代码开源,会使该内部私有仓库的地址被暴露
  • 私有仓库这种环境配置信息最好和代码分离。类似通过配置中心,将数据库地址等配置和代码分离。

我们完全可以将这个配置放到maven中。
可以通过mvn命令的启动参数来实现:

1
2
-DaltSnapshotDeploymentRepository=snapshots::default::https://YOUR_NEXUS_URL/snapshots
-DaltReleaseDeploymentRepository=releases::default::https://YOUR_NEXUS_URL/releases

更好的方法是将其配在settings.xml中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<settings>
[...]
<profiles>
<profile>
<id>nexus</id>
<properties>
<altSnapshotDeploymentRepository>snapshots::default::https://YOUR_NEXUS_URL/snapshots</altSnapshotDeploymentRepository>
<altReleaseDeploymentRepository>releases::default::https://YOUR_NEXUS_URL/releases</altReleaseDeploymentRepository>
</properties>
</profile>
</profiles>

<activeProfiles>
<activeProfile>nexus</activeProfile>
</activeProfiles>

</settings>

不要忘记也在<server></server>之间加上snapshotsreleases的账号。

参考资料

java - How to specify maven’s distributionManagement organisation wide? - Stack Overflow

本文永久链接 [ https://galaxyyao.github.io/2019/09/18/Maven-组织内部项目统一配置DistributionManagement/ ]