高阳Sunny

高阳Sunny 查看完整档案

杭州编辑中国科学院  |  联想之星 编辑SegmentFault  |  CEO 编辑 segmentfault.com/lives 编辑
编辑

SegmentFault 思否 CEO
C14Z.Group Founder
Forbes China 30U30

独立思考 敢于否定

曾经是个话痨... 要做一个有趣的人!

任何问题可以给我发私信或者发邮件 sunny@sifou.com

个人动态

高阳Sunny 关注了用户 · 今天 11:45

anyRTC @anyrtc

实时交互,万物互联,全球实时互动云服务商领跑者!

关注 2

高阳Sunny 赞了文章 · 今天 11:32

深入详解 Jetpack Compose | 优化 UI 构建

人们对于 UI 开发的预期已经不同往昔。现如今,为了满足用户的需求,我们构建的应用必须包含完善的用户界面,其中必然包括动画 (animation) 和动效 (motion),这些诉求在 UI 工具包创建之初时并不存在。为了解决如何快速而高效地创建完善的 UI 这一技术难题,我们引入了 Jetpack Compose —— 这是一个现代的 UI 工具包,能够帮助开发者们在新的趋势下取得成功。

在本系列的两篇文章中,我们将阐述 Compose 的优势,并探讨它背后的工作原理。作为开篇,在本文中,我会分享 Compose 所解决的问题、一些设计决策背后的原因,以及这些决策如何帮助开发者。此外,我还会分享 Compose 的思维模型,您应如何考虑在 Compose 中编写代码,以及如何创建您自己的 API。

Compose 所解决的问题

关注点分离 (Separation of concerns, SOC) 是一个众所周知的软件设计原则,这是我们作为开发者所要学习的基础知识之一。然而,尽管其广为人知,但在实践中却常常难以把握是否应当遵循该原则。面对这样的问题,从 "耦合" 和 "内聚" 的角度去考虑这一原则可能会有所帮助。

编写代码时,我们会创建包含多个单元的模块。"耦合" 便是不同模块中单元之间的依赖关系,它反映了一个模块中的各部分是如何影响另一个模块的各个部分的。"内聚" 则表示的是一个模块中各个单元之间的关系,它指示了模块中各个单元相互组合的合理程度。

在编写可维护的软件时,我们的目标是最大程度地减少耦合增加内聚

当我们处理紧耦合的模块时,对一个地方的代码改动,便意味对其他的模块作出许多其他的改动。更糟的是,耦合常常是隐式的,以至于看起来毫无关联的修改,却会造成了意料之外的错误发生。

关注点分离是尽可能的将相关的代码组织在一起,以便我们可以轻松地维护它们,并方便我们随着应用规模的增长而扩展我们的代码。

让我们在当前 Android 开发的上下文中进行更为实际的操作,并以视图模型 (view model) 和 XML 布局为例:

视图模型会向布局提供数据。事实证明,这里隐藏了很多依赖关系: 视图模型与布局间存在许多耦合。一个更为熟悉的可以让您查看这一清单的方式是通过一些 API,例如 findViewByID。使用这些 API 需要对 XML 布局的形式和内容有一定了解。

使用这些 API 需要了解 XML 布局是如何定义并与视图模型产生耦合的。由于应用规模会随着时间增长,我们还必须保证这些依赖不会过时。

大多数现代应用会动态展示 UI,并且会在执行过程中不断演变。结果导致应用不仅要验证布局 XML 是否静态地满足了这些依赖关系,而且还需要保证在应用的生命周期内满足这些依赖。如果一个元素在运行时离开了视图层级,一些依赖关系可能会被破坏,并导致诸如 NullReferenceExceptions 一类的问题。

通常,视图模型会使用像 Kotlin 这样的编程语言进行定义,而布局则使用 XML。由于这两种语言的差异,使得它们之间存在一条强制的分隔线。然而即使存在这种情况,视图模型与布局 XML 还是可以关联得十分紧密。换句话说,它们二者紧密耦合。

这就引出了一个问题: 如果我们开始用相同的语言定义布局与 UI 结构会怎样?如果我们选用 Kotlin 来做这件事会怎样?

由于我们可以使用相同的语言,一些以往隐式的依赖关系可能会变得更加明显。我们也可以重构代码并将其移动至那些可以使它们减少耦合和增加内聚的位置。

现在,您可能会以为这是建议您将逻辑与 UI 混合起来。不过现实的情况是,无论您如何组织架构,您的应用中都将出现与 UI 相关联的逻辑。框架本身并不会改变这一点。

不过框架可以为您提供一些工具,从而帮您更加简单地实现关注点分离: 这一工具便是 Composable 函数,长久以来您在代码的其他地方实现关注点分离所使用的方法,您在进行这类重构以及编写简洁、可靠、可维护的代码时所获得的技巧,都可以应用在 Composable 函数上。

Composable 函数剖析

这是一个 Composable 函数的示例:

@Composable
fun App(appData: AppData) {
  val derivedData = compute(appData)
  Header()
  if (appData.isOwner) {
    EditButton()
  }
  Body {
    for (item in derivedData.items) {
      Item(item)
    }
  }
}

在示例中,函数从 AppData 类接收数据作为参数。理想情况下,这一数据是不可变数据,而且 Composable 函数也不会改变: Composable 函数应当成为这一数据的转换函数。这样一来,我们便可以使用任何 Kotlin 代码来获取这一数据,并利用它来描述的我们的层级结构,例如 Header() 与 Body() 调用。

这意味着我们调用了其他 Composable 函数,并且这些调用代表了我们层次结构中的 UI。我们可以使用 Kotlin 中语言级别的原语来动态执行各种操作。我们也可以使用 if 语句与 for 循环来实现控制流,来处理更为复杂的 UI 逻辑。

Composable 函数通常利用 Kotlin 的尾随 lambda 语法,所以 Body() 是一个含有 Composable lambda 参数的 Composable 函数。这种关系意味着层级或结构,所以这里 Body() 可以包含多个元素组成的多个元素组成的集合。

声明式 UI

"声明式" 是一个流行词,但也是一个很重要的字眼。当我们谈论声明式编程时,我们谈论的是与命令式相反的编程方式。让我们来看一个例子:

假设有一个带有未读消息图标的电子邮件应用。如果没有消息,应用会绘制一个空信封;如果有一些消息,我们会在信封中绘制一些纸张;而如果有 100 条消息,我们就把图标绘制成好像在着火的样子......

使用命令式接口,我们可能会写出一个下面这样的更新数量的函数:

fun updateCount(count: Int) {
  if (count > 0 && !hasBadge()) {
    addBadge()
  } else if (count == 0 && hasBadge()) {
    removeBadge()
  }
  if (count > 99 && !hasFire()) {
    addFire()
    setBadgeText("99+")
  } else if (count <= 99 && hasFire()) {
    removeFire()
  }
  if (count > 0 && !hasPaper()) {
   addPaper()
  } else if (count == 0 && hasPaper()) {
   removePaper()
  }
  if (count <= 99) {
    setBadgeText("$count")
  }
}

在这段代码中,我们接收新的数量并且必须搞清楚如何更新当前的 UI 来反映对应的状态。尽管是一个相对简单的示例,这里仍然出现了许多极端情况,而且这里的逻辑也不简单。

作为替代,使用声明式接口编写这一逻辑则会看起来像下面这样:

@Composable
fun BadgedEnvelope(count: Int) {
  Envelope(fire=count > 99, paper=count > 0) {
    if (count > 0) {
      Badge(text="$count")
    }
  }
}

这里我们定义:

  • 当数量大于 99 时,显示火焰;
  • 当数量大于 0 时,显示纸张;
  • 当数量大于 0 时,绘制数量气泡。

这便是声明式 API 的含义。我们编写代码来按我们的想法描述 UI,而不是如何转换到对应的状态。这里的关键是,编写像这样的声明式代码时,您不需要关注您的 UI 在先前是什么状态,而只需要指定当前应当处于的状态。框架控制着如何从一个状态转到其他状态,所以我们不再需要考虑它。

组合 vs 继承

在软件开发领域,Composition (组合) 指的是多个简单的代码单元如何结合到一起,从而构成更为复杂的代码单元。在面向对象编程模型中,最常见的组合形式之一便是基于类的继承。在 Jetpack Compose 的世界中,由于我们使用函数替代了类型,因此实现组合的方法颇为不同,但相比于继承也拥有许多优点,让我们来看一个例子:

假设我们有一个视图,并且我们想要添加一个输入。在继承模型中,我们的代码可能会像下面这样:

class Input : View() { /* ... */ }
class ValidatedInput : Input() { /* ... */ }
class DateInput : ValidatedInput() { /* ... */ }
class DateRangeInput : ??? { /* ... */ }

View 是基类,ValidatedInput 使用了 Input 的子类。为了验证日期,DateInput 使用了 ValidatedInput 的子类。但是接下来挑战来了: 我们要创建一个日期范围的输入,这意味着需要验证两个日期——开始和结束日期。您可以继承 DateInput,但是您无法执行两次,这便是继承的限制: 我们只能继承自一个父类。 

在 Compose 中,这个问题变得很简单。假设我们从一个基础的 Input Composable 函数开始:

@Composable
fun <T> Input(value: T, onChange: (T) -> Unit) { 
  /* ... */
}

当我们创建 ValidatedInput 时,只需要在方法体中调用 Input 即可。我们随后可以对其进行装饰以实现验证逻辑:

@Composable
fun ValidatedInput(value: T, onChange: (T) -> Unit, isValid: Boolean) { 
  InputDecoration(color=if(isValid) blue else red) {
    Input(value, onChange)
  }
}

接下来,对于 DataInput,我们可以直接调用 ValidatedInput:

@Composable
fun DateInput(value: DateTime, onChange: (DateTime) -> Unit) { 
  ValidatedInput(
    value,
    onChange = { ... onChange(...) },
    isValid = isValidDate(value)
  )
}

现在,当我们实现日期范围输入时,这里不再会有任何挑战:只需要调用两次即可。示例如下:

@Composable
fun DateRangeInput(value: DateRange, onChange: (DateRange) -> Unit) { 
  DateInput(value=value.start, ...)
  DateInput(value=value.end, ...)
}

在 Compose 的组合模型中,我们不再有单个父类的限制,这样一来便解决了我们在继承模型中所遭遇的问题。

另一种类型的组合问题是对装饰类型的抽象。为了能够说明这一情况,请您考虑接下来的继承示例:

class FancyBox : View() { /* ... */ }
class Story : View() { /* ... */ }
class EditForm : FormView() { /* ... */ }
class FancyStory : ??? { /* ... */ }
class FancyEditForm : ??? { /* ... */ }

FancyBox 是一个用于装饰其他视图的视图,本例中将用来装饰 Story 和 EditForm。我们想要编写 FancyStory 与 FancyEditForm,但是如何做到呢?我们要继承自 FancyBox 还是 Story?又因为继承链中单个父类的限制,使这里变得十分含糊。

 

相反,Compose 可以很好地处理这一问题:

@Composable
fun FancyBox(children: @Composable () -> Unit) {
  Box(fancy) { children() }
}
@Composable fun Story(…) { /* ... */ }
@Composable fun EditForm(...) { /* ... */ }
@Composable fun FancyStory(...) {
  FancyBox { Story(…) }
}
@Composable fun FancyEditForm(...) {
  FancyBox { EditForm(...) }
}

我们将 Composable lambda 作为子级,使得我们可以定义一些可以包裹其他函数的函数。这样一来,当我们要创建 FancyStory 时,可以在 FancyBox 的子级中调用 Story,并且可以使用 FancyEditForm 进行同样的操作。这便是 Compose 的组合模型。

封装

Compose 做的很好的另一个方面是 "封装"。这是您在创建公共 Composable 函数 API 时需要考虑的问题: 公共的 Composable API 只是一组其接收的参数而已,所以 Compose 无法控制它们。另一方面,Composable 函数可以管理和创建状态,然后将该状态及它接收到的任何数据作为参数传递给其他的 Composable 函数。

现在,由于它正管理该状态,如果您想要改变状态,您可以启用您的子级 Composable 函数通过回调告知当前改变已备份。

重组

"重组" 指的是任何 Composable 函数在任何时候都可以被重新调用。如果您有一个庞大的 Composable 层级结构,当您的层级中的某一部分发生改变时,您不会希望重新计算整个层级结构。所以 Composable 函数是可重启动 (restartable) 的,您可以利用这一特性来实现一些强大的功能。

举个例子,这里有一个 Bind 函数,里面是一些 Android 开发的常见代码:

fun bind(liveMsgs: LiveData<MessageData>) {
  liveMsgs.observe(this) { msgs ->
    updateBody(msgs)
  }
}

我们有一个 LiveData,并且希望视图可以订阅它。为此,我们调用 observe 方法并传入一个 LifecycleOwner,并在接下来传入 lambda。lambda 会在每次 LiveData 更新被调用,并且发生这种情况时,我们会想要更新视图。

使用 Compose,我们可以反转这种关系。

@Composable
fun Messages(liveMsgs: LiveData<MessageData>) {
 val msgs by liveMsgs.observeAsState()
 for (msg in msgs) {
   Message(msg)
 }
}

这里有一个相似的 Composable 函数—— Messages。它接收了 LiveData 作为参数并调用了 Compose 的 observeAsState 方法。observeAsState 方法会把 LiveData<T> 映射为 State<T>,这意味着您可以在函数体的范围使用其值。State 实例订阅了 LiveData 实例,这意味着 State 会在 LiveData 发生改变的任何地方更新,也意味着,无论在何处读取 State 实例,包裹它的、已被读取的 Composable 函数将会自动订阅这些改变。结果就是,这里不再需要指定 LifecycleOwner 或者更新回调,Composable 可以隐式地实现这两者的功能。

总结

Compose 提供了一种现代的方法来定义您的 UI,这使您可以有效地实现关注点分离。由于 Composable 函数与普通 Kotlin 函数很相似,因此您使用 Compose 编写和重构 UI 所使用的工具与您进行 Android 开发的知识储备和所使用的工具将会无缝衔接。

查看原文

赞 2 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月20日

阿里Teambition,无可挑剔的网盘!

一、简介:
最近看到了阿里出网盘了,结果没赶上最后一波内测,只能预约公测了,结果看到是否有开发者专属内测申请渠道,一次通过,看到这封邮件太激动了!

![image.png](/img/bVcHFl5)

二、容量说明:
目前内测期间是赠送2T,公测用户空间有多大未知,不过应该是很惊喜的,可以满足普通用户日常需求。
三、客户端说明

1、目前客户端只存在安卓和IOS端,PC端还没发布。
2、界面相对比较清爽,简洁。
3、web端支持直接上传文件夹。

三、上传测速

在500MB四川电信测速环境下,上传速度达到6-8MB/S,稳定在6MB每秒(这个速度不知道会不会根据宽度而提高,不过这个速度很惊喜了,ps:这里说下,一般来说家庭宽度上传宽度会低于下载带宽,我们通常说的200MB指下载带宽,上传带宽有下载的十分之一就不错了)。

四、下载测速(四川电信)
在本机500MB带宽下,下载速度40+MB/S!

![image.png](/img/bVcHFpq)

在千兆电信带宽下,下载速度达到80+MB/S!
这里没图,是使用朋友宽度测速的,下载3.2G文件不足40秒下载完毕,通过下载链接可以看出是阿里云的OSS。
五、文件分享

截止发稿,文件分享正在升级,也就没法截图了。文件分享可设定分享时长,其他用户无需登录就可以直接下载你分享的文件!

六、直链
1、阿里网盘是直链,下载链接是固定的,做资源下载,论坛的朋友懂得吧!

![image.png](/img/bVcHFuJ)

2、阿里网盘视频可以直接在线播放,无需转存,某度的在线播放清晰度大家都体验过吧,对比如下(左边为某度):

![image](/img/bVcHFwg)

3、1080视频在线播放截图来两张,播放清晰度无损(清晰度降低了一些,不是播放清晰度不行,是上传只能上传低于4MB的图片,所以压缩了图片)

![image](/img/bVcHFxw)
![image](/img/bVcHFyB)

4、做视频播放的同学有福了!!
5、有评测朋友说下载直链在一天后有效,但是我测试发现长期有效,至于怎么获取这个链接,官方没有发布出来,不过应该难不倒开发者吧。
六、安全方面

从事网络安全行业,对网站进行测试,未发现大安全隐患,本人小菜一个,如果有发现安全问题可提交阿里SRC。

七、总结

1、时间原因,对使用体验测试较少,较多时间用于安全测试及功能测试。
2、总体来说,上传和下载速度,清爽的界面,都是无可挑剔的。
3、webdav和API一直是开发者期待的功能,但目前官方未发表关于这方面消息,但是可以看到官方在不断采纳用户意见,希望阿里网盘越做越好。
4、至于办公协作,前段时间一直处于忙碌,最近有项目,准备使用Teambition的办公协作试试。
5、以上只代表个人体验,如有不实,可联系我纠正。
查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月20日

应用架构之道:分离业务逻辑和技术细节

头图.png

作者 | 张建飞  阿里巴巴高级技术专家

架构

什么是架构?

关于架构这个概念很难给出一个明确的定义,也没有一个标准的定义。

硬是要给一个概述,我认为架构就是对系统中的实体以及实体之间的关系所进行的抽象描述。

架构始于建筑,是因为人类发展(原始人自给自足住在树上,也就不需要架构),分工协作的需要,将目标系统按某个原则进行切分,切分的原则,是要便于不同的角色进行并行工作。

为什么需要架构?

有系统的地方就需要架构,大到航空飞机,小到一个电商系统里面的一个功能组件都需要设计和架构。

我很喜欢《系统架构:复杂系统的产品设计与开发》里面的一句话:结构良好的创造活动要优于毫无结构的创造活动

与之相对应的,现在很多敏捷思想提倡 no design,只要 work 就好。期待好的架构可以在迭代中自然涌现。这个想法有点太理想化了,在现实中,只要能 work 的代码,工程师是很少有动力去重构和优化的。

架构师的职责

作为架构师,我们最重要的价值应该是“化繁为简”。但凡让事情变得更复杂,让系统变得更晦涩难懂的架构都是值得商榷的。

架构师的工作就是要努力训练自己的思维,用它去理解复杂的系统,通过合理的分解和抽象,使哪些系统不再那么难懂。我们应该努力构建易懂的架构,使得在系统上工作的其他人员(例如设计者、实现者、操作员等)可以较为容易地理解这个系统。

软件架构

软件架构是一个系统的草图。软件架构描述的对象是直接构成系统的抽象组件。各个组件之间的连接则明确和相对细致地描述组件之间的通信。在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口来实现。

软件架构为软件系统提供了一个结构、行为和属性的高级抽象,由构件的描述、构件的相互作用、指导构件集成的模式以及这些模式的约束组成。软件架构不仅显示了软件需求和软件结构之间的对应关系,而且指定了整个软件系统的组织和拓扑结构,提供了一些设计决策的基本原理。

软件架构的核心价值应该只围绕一个核心命题:控制复杂性。他并不意味着某个特定的分层结构,某个特定的方法论(贫血、DDD 等)。

软件架构分类

在介绍应用架构之前,我们先来看一下软件架构的分类。

随着互联网的发展,现在的系统要支撑数亿人同时在线购物、通信、娱乐的需要,相应的软件体系结构也变得越来越复杂。软件架构的含义也变得更加宽泛,我们不能简单地用一个软件架构来指代所有的软件架构工作。按照我个人理解,我将软件架构划分为:

1.png

业务架构:由业务架构师负责,也可以称为业务领域专家、行业专家。业务架构属于顶层设计,其对业务的定义和划分会影响组织结构和技术架构。例如,阿里巴巴在没有中台部门之前,每个业务部门的技术架构都是烟囱式的,淘宝、天猫、飞猪、1688 等各有一套体系结构。而后,成立了共享平台事业部,打通了账号、商品、订单等体系,让商业基础实施的复用成为可能。

应用架构:由应用架构师负责,他需要根据业务场景的需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性要求(性能、安全、稳定性等)。

分布式系统架构:分布式系统基本是稍具规模业务的必选项。它需要解决服务器负载,分布式服务的注册和发现,消息系统,缓存系统,分布式数据库等问题,同时架构师要在 CAP(Consistency,Availability,Partition tolerance)之间进行权衡。

数据架构:对于规模大一些的公司,数据治理是一个很重要的课题。如何对数据收集、数据处理提供统一的服务和标准,是数据架构需要关注的问题。其目的就是统一数据定义规范,标准化数据表达,形成有效易维护的数据资产,搭建统一的大数据处理平台,形成数据使用闭环。

物理架构:物理架构关注软件元件是如何放到硬件上的,包括机房搭建、网络拓扑结构,网络分流器、代理服务器、Web服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。

运维架构:负责运维系统的规划、选型、部署上线,建立规范化的运维体系。

典型应用架构

分层架构

分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。常见的分层结构如下图所示:

2.png

CQRS

CQS(Command Query Separation,命令查询分离),最早来自于 Betrand Meyer(Eiffel 语言之父,OCP 提出者)提出的概念。其基本思想在于,任何一个对象的方法可以分为两大类:

  • 命令(Command): 不返回任何结果(void),但会改变对象的状态。
  • 查询(Query): 返回结果,但是不会改变对象的状态,对系统没有副作用。

3.png

六边形架构

六边形架构是 Alistair Cockburn 在 2005 年提出,解决了传统的分层架构所带来的问题,实际上它也是一种分层架构,只不过不是上下,而是变成了内部和外部(如下图所示)。

4.png

六边形架构又称为端口-适配器架构,这个名字更容器理解。六边形架构将系统分为内部(内部六边形)和外部,内部代表了应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。

适配器分为两种类型(如下图所示),左侧代表 UI 的适配器被称为主动适配器(Driving Adapters),因为是它们发起了对应用的一些操作。而右侧表示和后端工具链接的适配器,被称为被动适配器(Driven Adapters),因为它们只会对主适配器的操作作出响应。

5.png

洋葱圈架构

洋葱架构与六边形架构有着相同的思路,它们都通过编写适配器代码将应用核心从对基础设施的关注中解放出来,避免基础设施代码渗透到应用核心之中。这样应用使用的工具和传达机制都可以轻松地替换,可以一定程度地避免技术、工具或者供应商锁定。

不同的是洋葱架构还告诉我们,企业应用中存在着不止两个层次,它在业务逻辑中加入了一些在领域驱动设计的过程中被识别出来的层次(Application,Domain Service,Domain model,Infrastructure等)。

另外,它还有着脱离真实基础设施和传达机制应用仍然可以运行的便利,这样可以使用 mock 代替它们方便测试。

6.png

在洋葱架构中,明确规定了依赖的方向:

  • 外层依赖内层;
  • 内层对外层无感知。

COLA 应用架构

COLA 架构是我团队自主研发的应用架构,目前已经开源。在 COLA 的设计中,我们充分汲取了经典架构的优秀思想。除此之外,我们补充了规范设计和扩展设计,并且使用 Archetype 的方式,将架构固化下来,以便可以快速的在开发中使用。

COLA 开源地址:https://github.com/alibaba/COLA

分层设计

COLA 的分层是一种改良了的三层架构。主要是将传统的业务逻辑层拆分成应用层、领域层和基础实施层。如下图所示,左边是传统的分层架构,右边是 COLA 的分层架构。

7.png

其每一层的作用范围和含义如下:

1)展现层(Presentation Layer):负责以 Rest 的格式接受 Web 请求,然后将请求路由给 Application 层执行,并返回视图模型(View Model),其载体通常是 DTO(Data Transfer Object);

2)应用层(Application Layer):主要负责获取输入,组装上下文,做输入校验,调用领域层做业务处理,如果需要的话,发送消息通知。当然,层次是开放的,若有需要,应用层也可以直接访问基础实施层;

3)领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Entities)的函数对外部提供业务逻辑的计算和处理;

4)基础实施层(Infrastructure Layer)主要包含 Tunnel(数据通道)、Config 和 Common。这里我们使用 Tunnel 这个概念来对所有的数据来源进行抽象,这些数据来源可以是数据库(MySQL,NoSql)、搜索引擎、文件系统、也可以是 SOA 服务等;Config 负责应用的配置;Common 是通用的工具类。

扩展设计

对于只有一个业务的简单场景,对扩展性的要求并不突出,这也是为什么扩展设计常被忽略的原因,因为我们大部分的系统都是从单一业务开始的。但是随着业务场景越来越复杂,代码里面开始出现大量的if-else逻辑。此时除了常规的策略模式以外,我们可以考虑在架构层面提供统一的扩展解决方案。

在扩展设计中,我们提炼出两个重要的概念,一个是业务身份,另一个是扩展点

业务身份是指业务在系统唯一标识一个业务或者一个场景的标志。在具体实现中,我们使用 BizCode 来表示业务身份,其中 BizCode 采用类似 Java 包名命名空间的方式。例如,我们可以用“ali.tmall”表示阿里天猫业务,用“ali.tmall.car” 表示阿里天猫的汽车业务,而用"ali.tmall.car.aftermarket"代表这是阿里天猫的汽车业务的后市场场景。

每个业务或者场景都可以实现一个或多个扩展点(ExtensionPoint),也就是说一个业务身份加上一个扩展点,可以唯一地确定一个扩展实现(Extension)。而这个业务身份和扩展点的组合,我们将其称之为扩展坐标(ExtensionCoordinate),如下图所示。

8.png

这样,通过业务身份+扩展点,我们就可以从框架层面实现对不同租户,不同业务,不同场景的扩展定制了。整个阿里业务中台正是基于这个思想,实现的多业务支撑的。

规范设计

任何事物都是规则性和随机性的组合。规范的意义就在于我们可以将规则性的东西固化下来,尽量减少随心所欲带来的复杂度,一致性可以降低系统复杂度。从命名到架构皆是如此,而架构本身就是一种规范和约束,破坏这个约束,也就破坏了架构。

COLA 制定了一些列的规范:包括组件(Module)结构、包(Package)结构、命名等。

比如对于组件,我们要求使用 COLA 的应用都应该遵循如下图所示的组件划分:

9.png

COLA 架构总览

在架构思想上,COLA 主张像六边形架构那样,使用端口-适配器去解耦技术细节;主张像洋葱圈架构那样,以领域为核心,并通过依赖倒置反转领域层的依赖方向。最终形成如下图所示的组件关系。

10.png

换一个视角,从 COLA 应用处理响应一个请求的过程来看。COLA 使用了 CQRS 来分离命令和查询的职责,使用扩展点和元数据来提升应用的扩展性。整个处理流程如下图所示:

11.png

应用架构的核心

纵观上面介绍的所有应用架构,我们可以发现一个共同点,就是“核心业务逻辑和技术细节分离”。

12.png

是的,六边形架构、洋葱圈架构、以及 COLA 架构的核心职责就是要做核心业务逻辑和技术细节的分离和解耦。

试想一下,业务逻辑和技术细节糅杂在一起的情况,所有的代码都写在 ServiceImpl 里面,前几行代码是做 validation 的事,接下来几行是做 convert 的事,然后是几行业务处理逻辑的代码,穿插着,我们需要通过 RPC 或者 DAO 获取更多的数据,拿到数据后,又是几行 convert 的代码,在接上一段业务逻辑代码,然后还要落库,发消息.....等等。

再简单的业务,按照上面这种写代码的方式,都会变得复杂,难维护。

因此,我认为应用架构的核心使命就是要分离业务逻辑和技术细节。让核心业务逻辑可以反映领域模型和领域应用,可以复用,可以很容易被看懂。让技术细节在辅助实现业务功能的同时,可以被替换。

最后我们发现,应用架构的道就是:“让上帝的归上帝,凯撒的归凯撒。”

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

PHP8.x 你必须知道的这些新特性

前言

Hello 大家好,我是CrazyCodes,距离上次发文已经过去4个月的时间,今年是悲惨的一年,也是奋发的一年,我会发布一些更好更实用的文章与大家分享,谢谢大家一直以来的支持。

本篇是我参加《2020 PHP开发者峰会》 Nikita分享内了解到的一些知识与大家分享

Nikita 是PHP8的核心开发者。
PHP8的版本会在今年11月26日与各位开发者见面,敬请期待

JIT

值得被提起的则是JIT新的特性,它会将PHP代码转换为传统的机器码,而并非通过zend虚拟机来运行,这样大大的增加了运行速度,但并不向下兼容,这意味着你不能通过像PHP5升级到PHP7那样获得该特性。

JIT可以通过php.ini去设置,例如这样

opcache.jit=on // on 代表打开,则off代表关闭

注解

PHP8版本彻底把注解扶正,当然在这之前像 Symfony,hyperf通过php-parser加入注解的使用方法,但这毕竟不属于PHP8内核真正的部分,在PHP8的版本中,但依旧需要反射

new ReflecationProperty(User::class,"id");

去获取到注解部分,看来注解在PHP的历史长河中还是需要继续不断完善的。

类中的成员变量

小的知识点

在PHP8之前,我们一般会这样定义一个类,首先要设置成员变量,然后在构造或者某一个方法为它赋值。

class User{
    public $username;
    public $phone;
    public $sex;
    
    public function __contruct(
        $username,$phone,$sex
    ){
        $this->username = $username;
        $this->phone = $phone;
        $this->sex = $sex;
    }
}

但在PHP8上,我们可以这样做

class User{
    public function __contruct(
        public string $username = "zhangsan",
        public string $phone = "110110";
        public string $sex = "男"
    ){}
}

命名参数

当我们创建一个函数时,例如

function roule($name,$controller,$model){
    // ... code
}

在调用这个函数时,我们需要顺序输入参数

roule("user/login","UserController","login");

但在PHP8中,我们可以这样做

roule(name:"user/login",controller:"UserController",model:"login");

因为可以需要输入参数名来区分传入的字段,那么在一些函数中,类比中间某项这段需要默认值,那我们就可以跳过这个字段

function roule($name,$controller="UserController",$model){
    // ... code
}
roule(name:"user/login",model:"login");

当然也可以以传统方式与其相结合

roule("user/login",model:"login");

联合类型

在PHP7中,我们在强制函数返回类型时是这样做的

function create() : bool

那么在PHP8中你可以使用多种预测类型

function create() : bool|string

当然在传参时也可以这样做

function create(bool|string $userId)

并且也可以设置类型NULL和TRUE,FALSE了。

总结

以上是PHP8主要的一些特性,所有表达和案例都是在Nikita的基础上描述的,并没有直接照搬,当然Nikita的演讲并不仅仅只有这些,为了保持对峰会主办方的尊重,还请各位移步至
https://www.itdks.com/Home/Ac...
观看Nikita的完整演讲。

致谢

感谢你看到这里,希望本篇文章对你的技术生涯多一分动力,谢谢!

查看原文

赞 14 收藏 5 评论 3

高阳Sunny 赞了文章 · 10月19日

从理论到工具:带你全面了解自动化测试框架

软件行业正迈向自主、快速、高效的未来。为了跟上这个高速前进的生态系统的步伐,必须加快应用程序的交付时间,但不能以牺牲质量为代价。快速实现质量是必要的,因此质量保证得到了很多关注。为了满足卓越的质量和更快的上市时间的需求,自动化测试将被优先考虑。对于微型、小型和中型企业(SMEs)来说,自动化自身的测试过程是非常必要的,而最关键的方面是选择正确的自动化测试框架。

什么是自动化测试框架?

自动化测试框架是为自动化测试脚本提供执行环境的脚手架。框架为用户提供了各种优势,帮助他们有效地开发、执行和报告自动化测试脚本。它更像是一个专门为自动化组织的测试而创建的系统。简而言之,我们可以说框架是各种指导方针、编码标准、概念、过程、实践、项目层次、模块化、报告机制、测试数据注入等要素的建设性混合,以此支撑自动化测试。因此,用户在自动化应用程序以利用各种生产性结果时可以遵循这些指导原则。

这些优势可以是不同的形式,如易于编写脚本、可伸缩性、模块化、可理解性、过程定义、可重用性、成本、维护等。因此,为了能够获得这些好处,建议开发人员使用一个或多个自动化测试框架。此外,当有一群开发人员在同一个应用程序的不同模块上工作时,以及当我们希望避免每个开发人员实现自己的自动化方法的情况下,需要一个统一的标准测试自动化框架。

自动化测试框架的类型

市场上的自动化测试框架可能因支持不同的关键因素(如可重用性、易维护性等)而有所不同。如以下几种类型:

●基于模块的测试框架
●测试库架构框架
●数据驱动测试框架
●关键字驱动测试框架
●混合测试框架
●行为驱动开发框架

自动化测试框架的优势

除了自动化测试所需的最少的手动干预外,使用测试自动化框架还有许多优点:
更快的上市时间:通过允许测试用例的持续执行,使用一个好的测试自动化框架有助于减少应用程序的上市时间。一旦自动化,测试库的执行将比手动测试更快,运行时间也更持久。
早期缺陷检测:对于测试团队来说,软件缺陷的文档记录变得相当容易。它提高了总体开发速度,同时确保了跨区域的正确功能。问题发现的越早,解决成本就越低,采用自动化测试框架的效益也就越高。

提高测试效率:测试占据了整个开发生命周期的重要部分。即使是总体效率的最轻微的改进也会对项目的整个时间框架产生巨大的影响。尽管最初的设置时间较长,但自动化测试最终所占用的时间要少得多。它们实际上可以在无人值守的情况下运行,在进程的最后时刻对结果进行监视。

更高的投资回报率:虽然最初的投资可能较高,但自动化测试可以长期为组织节省支出。这是由于运行测试所需的时间减少,从而导致工作质量更高。这反过来降低了发布后的故障概率,从而降低了项目成本。

更高的测试覆盖率:在自动化测试中,可以对应用程序执行更多的测试,这将带来更高的测试覆盖率。增加测试覆盖率可以测试更多的特性和应用程序的质量。

自动化测试的可重用性:在测试自动化中,测试用例的重复性可以帮助软件开发人员评估程序的反应,以及相对简单的设置配置。自动化测试用例可以通过不同的方法来使用,因为它们是可重用的。

十大自动化测试框架

1.机器人框架

如果是希望在测试自动化工作中使用python测试自动化框架,Robot框架是最佳选择。Robot框架基于Python,但也可以使用Jython(Java)或IronPython(.NET)。Robot框架使用关键字驱动的方法来简化测试的创建。Robot框架还可以测试MongoDB、FTP、Android、Appium等。它有许多测试库,包括Selenium WebDriver库和其他有用的工具。它有很多API来帮助它尽可能地扩展。Robot框架使用的关键字方法对于那些已经熟悉其他基于供应商的关键字驱动的测试工具的测试人员非常有用,这使得他们更容易过渡到开源。

2.网络驱动(WebDriverIO)

WebdriverIO是一个基于Node.js的自动化测试框架。它有一个集成的测试运行器,可以为web应用程序和本地移动应用程序运行自动化测试。同时,它可以在WebDriver协议和Chrome Devtools协议上运行,使它对基于Selenium WebDriver的跨浏览器测试或基于Chromium的自动化都有效。由于WebDriverIO是开源的,你可以得到一堆插件来满足你的自动化需求。“Wdio安装向导”使安装简单和容易。

3.Citrus

Citrus是一个开源框架,您可以使用它自动化任何消息传递协议或数据格式的集成测试。对于任何类型的消息传递,如REST、HTTP、SOAP或JMS,Citrus框架将适合测试消息传递集成。如果您需要与用户界面交互,然后验证后端流程,那么可以将Citrus与Selenium集成。例如,如果您必须单击“发送电子邮件”按钮并在后端验证电子邮件是否已收到,柑橘可以接收此电子邮件或UI触发的JMS通信,并验证后端结果,所有这些都在一个测试中完成。

4.Cypress

Cypress是一个以开发人员为中心的测试自动化框架,它使测试驱动开发(TDD)成为开发人员的现实。它的设计原则是能够打包和捆绑所有东西,使整个端到端测试体验愉快和简单。Cypress的架构与Selenium不同;Selenium WebDriver远程运行在浏览器外部,而Cypress运行在浏览器内部。这种方法有助于理解浏览器内部和外部发生的一切,从而提供更一致的结果。它不需要您处理对象序列化或在线协议,同时为您提供对每个对象的本机访问。当您将应用程序拉入浏览器时,Cypress可以同步通知您浏览器内发生的每一件事情,这样您就可以本机访问每个DOM元素。它还使得在应用程序中放置调试器变得很容易,这反过来又使开发人员工具的使用变得更容易。

5.Selenium

web应用程序最流行的开源测试自动化框架之一。Selenium还可以作为许多其他测试工具的基础,因为它具有跨平台和跨浏览器的功能。Selenium支持多种编程语言,如Java、C#、PHP、Python、Ruby等。它易于维护,因为它拥有最大的在线支持网络之一。Selenium可以通过广泛的库和api进行高度扩展,以满足每个人的需求和需求。Selenium是测试人员的首选,因为它可以编写更高级的测试脚本来满足各种复杂程度。它为测试编写提供了一个回放工具,无需学习特定的脚本语言。

6. Cucumber

它是一个跨平台的行为驱动开发(BDD)工具,用于编写web应用程序的验收测试。Cucumber可以快速且容易地设置执行,并允许在测试中重用代码。它支持Python、PHP、Perl、.NET、Scala、Groovy等语言,以易于阅读和理解的格式实现函数验证的自动化。一个好的特性是规范和测试文档都被上传到一个最新的文档中。Cucumber使不熟悉测试的业务涉众更容易阅读代码,因为他们可以轻松地阅读代码,因为测试报告是用商业可读的英语编写的。该代码可以与Selenium、Watir、Capybara等其他框架一起使用。

7.Gauge

它是一个开源工具无关的测试自动化框架,适用于Mac、Linux和Windows。从事TDD和BDD工作的人会喜欢Gauge专注于创建动态/可执行文档。规范——量规自动化测试是在现有的ide(如visualstudio和Eclipse)中使用C、Java和Ruby的降价语言编写的。Gauge的功能也可以通过对插件的支持进行扩展。它是作为一个BYOT(自带工具)框架开发的。因此,您可以使用Selenium,也可以使用任何其他工具来驱动测试UI或API测试。如果你想要一个可读的非BDD方法来实现自动化,你应该试试Gauge。

8.Serenity

如果您正在寻找一个与cumber和JBehave等行为驱动开发(BDD)工具集成的基于Java的框架,那么Serenity可能是适合您的工具。它的目的是使编写自动化验收和回归测试更容易。它还允许您将测试场景保持在较高级别,同时在报告中容纳较低级别的实现细节。
Serenity充当Selenium WebDriver和BDD工具的包装器。它抽象了许多您有时需要编写的样板代码,这使得编写BDD和Selenium测试变得更容易。Serenity还提供了大量的内置功能,例如处理并行运行的测试、WebDriver管理、截屏、管理步骤之间的状态、促进Jira集成,所有这些都不需要编写一行代码。

9.Carina

Carina使用流行的开源解决方案构建,如Appium、TestNG和Selenium,这减少了对特定技术栈的依赖。您可以测试移动应用程序(本机、web、混合)、web应用程序、REST服务和数据库。Carina框架支持MySQL、sqlserver、Oracle、PostgreSQL等不同类型的数据库,提供了MyBatis ORM框架实现DAO层的惊人体验。它支持所有流行的浏览器和移动设备,并且在IOS/Android之间重用测试自动化代码高达80%。API测试基于Freemarker模板引擎,它在生成REST请求方面提供了极大的灵活性。Carina是跨平台的,可以在Unix或Windows操作系统上轻松地执行测试。

10.ZTF

Zentao Testing Framework,简称ZTF,是一款开源自动化测试管理框架。与市面上已有的自动化测试框架相比,ZTF更聚焦于自动化测试的管理功能。ZTF提供了自动化测试脚本的定义、管理、驱动、执行结果的回传、Bug的创建以及和其他自动化测框架的集成。ZTF使用go语言开发,可以支持各种平台。ZTF支持常见的编程语言,您可以选择您喜欢用的语言来开发自动化测试脚本。通过禅道自研的ZTF自动化测试工具,可很好地驱动8种单元测试框架、3种自动化测试框架来执行测试,并把最终结果回传给禅道,进行统一的报告展示。禅道ZTF打通了项目管理和持续集成工具之间的沟壑,贯穿持续集成、持续测试、持续部署等DevOps生命周期的不同阶段。

总结

以上列出的工具大多是已成熟且流行的,它们使用AI/ML提供了测试自动化功能,以解决组织现在面临的快速交付及质量的挑战。此列表还包括提供API和服务测试的工具,这些工具对于成功的DevOps转换至关重要。人工智能、无代码、大数据和物联网测试等新兴技术正在提高测试自动化的效率,同时也为现有的工具和新的参与者创造了机会,使其能够为测试社区带来价值。

自动化工具的选择不仅应该满足当前需求,还应该关注潜在的趋势和改进。有效的测试自动化工具应该支持基本的优化、数据生成、更智能的解决方案和分析。到目前为止,组织中的测试自动化水平很低,在14%到18%之间。但是组织正在努力将自动化覆盖率提高到80%。API和服务测试也是未来发展的趋势。

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

如何让一套代码适配所有iOS设备尺寸?

简介: 随着移动互联网设备和技术的发展,各种移动设备屏幕尺寸层出不穷,折叠屏、分屏、悬浮窗等等,面对越来越多样的屏幕,如果为每种尺寸单独进行适配,不仅费时费力,还会增加端侧代码的开发与维护压力。如何让一套代码适配所有尺寸变化,增强App的通用能力?阿里巴巴文娱技术 氚雨 将分享优酷APP在iOS响应式布局技术上的实践和落地。

image.png

响应式是基于同一套代码,开发一个APP能够兼容多尺寸、多终端设备的显示,能够动态调整页面的布局以及容器的布局,充分利用当前屏幕的尺寸,为用户提供更好的浏览体验,提升APP开发效率和迭代效率。

一 iOS布局尺寸预研

当下,iOS端的主要尺寸类型有五种:iPhone、iPad竖屏、iPad横屏、iPad浮窗、iPad分屏。通常,App是按iPhone尺寸开发的,需要适配剩余的四种iPad尺寸。

iPad横、竖屏比较常见,旋转设备即可,比较特殊的是浮窗和分屏模式。自苹果iPad iOS 9开始,用户在打开一个应用时,从最底部上滑打开Dock,即可拖拽另一个App进入浮窗模式:

640.gif

在支持分屏的iPad上拖拽到更边缘的地方即可开启分屏模式:

640 (1).gif.gif")

其中浮窗模式所有升级iOS 9的设备都支持,分屏模式只有最新版的硬件设备iPad mini 4、iPad Air 2及iPad Pro支持:

image.png

二 优酷iOS响应式方案

响应式布局的核心是设计统一的适配规则,并在屏幕尺寸发生变化时按布局规则重新布局,以适配不同屏幕尺寸,而大多数App在开发时一般只有适配iPhone的版本,在通过响应式适配更多机型时主要要解决三个方面的问题,即如何获取、更新响应式状态以进行对应的适配,如何计算在不同屏幕宽度下App内容的宽度、列数等布局参数,如何进行响应式下的数据处理以解决较难适配的组件、减少页面留白等,基于此我们开发了响应式布局SDK,负责统一管理响应式状态、处理布局逻辑、裁剪映射数据等。

image.png

1 响应式App配置

App除了配置为universal版之外,要支持浮窗或分屏模式还需要进行一些配置:

(1)需要提供LaunchScreen.storyboard作为启动图,由于App支持的运行尺寸太多,不再适合用图片作为启动图。

(2)需要在info.plist中配置支持所有屏幕方向:

image.png

(3)注意不能勾选Requires full screen配置项或配置UIRequiresFullScreen为YES,如此会声明App要求全屏运行,自然表示不支持浮窗或分屏:

image.png

(4)支持分屏要求App的主Window需要使用系统UIWindow,不能继承,并且要通过init方法或initWithFrame:[UIScreen mainScreen].bounds方式初始化。

通过以上步骤开启浮窗、分屏能力后,在App内就无法再通过相关代码控制设备方向,以往通过如下代码可控制ViewController为竖屏,而支持分屏后如下方法系统不再调用,默认所有ViewController支持所有屏幕方向:

image.png

如下强制设置屏幕方向的黑方法也已失效:

image.png

这种设计的主要原因是,当一个App支持分屏后,就不再单独占用整个屏幕,当另一个App同时运行时,同一块屏幕不可能出现一个横屏、另一个竖屏。此类问题没有完美的解决方案,为了保证用户体验,支持分屏的App必须所有页面适配所有屏幕方向,这也体现了苹果对用户体验的极致追求,参见DeveloperForums中开发人员的讨论:
https://developer.apple.com/forums/thread/19578

2 响应式SDK

响应式状态管理

响应式状态提供了当前是否开启响应式、响应式布局尺寸类型、当前布局window尺寸等相关状态量,响应式SDK会在屏幕尺寸变化后更新响应式状态,并通过系统通知和自定义通知机制,通知相关业务方。

// 响应式开启关闭状态
typedefNS_ENUM(NSInteger, YKRLLayoutStyle) {   
    YKRLLayoutStyleNormal =0,        // 响应式状态关闭   
    YKRLLayoutStyleResponsive =1,    // 响应式状态开启}; 
    
// 响应式屏幕尺寸类型,页面可依据此类型区分是否分屏等
typedefNS_ENUM(NSInteger, YKRLLayoutSizeType) {   
    YKRLLayoutSizeTypeS =0,      // eg. phone pad浮窗   
    YKRLLayoutSizeTypeL =1,      // pad   
    YKRLLayoutSizeTypeXL =2,     // 预留
}; 

// 响应式屏幕状态类型(一共有十种类型)
typedefNS_OPTIONS(NSUInteger, YKRLLayoutScreenType) {   
    YKRLLayoutScreenTypeUnknown = (1<<0),          //未知   
    YKRLLayoutScreenTypePortrait = (1<<1),         //竖屏全屏
    YKRLLayoutScreenTypeLandscapeLeft = (1<<2),    //横屏全屏左
    … …
};

响应式SDK声明了YKRLLayoutStyle、YKRLLayoutSizeType、YKRLLayoutScreenType三种枚举状态标记当前的响应式状态,分别表示响应式开启关闭状态,当前尺寸类型及具体屏幕类型,一般业务方只需要获取是否是响应式设备状态,对于在不同宽度下页面布局不一致的业务方可以通过尺寸类型状态进行区分适配,而对于需要具体知道当前屏幕状态的业务方可以通过屏幕类型获取,屏幕类型只包含当前iOS设备已支持的屏幕状态,随着设备类型的丰富,如出现折叠屏等,屏幕类型会作相应扩展。每当设备旋转或用户开启分屏时,响应式SDK都会在系统回调中更新当前响应式状态,并通知业务方响应式状态的改变。

响应式布局规则

优酷响应式布局规则主要包含列数适配规则、宽度适配规则等,比如多列均分组件的列数在不同屏幕宽度下是可变的,响应式SDK会根据当前的响应式状态输出合适的布局列数等,对于每一个布局规则,响应式SDK中都有相应的布局适配逻辑,响应式布局规则满足优酷App整体UI规范,业务方直接指定自己所需要的规则即可,除少数特殊规则之外,大部分布局规则都用于组件列数和组件宽度布局,此类响应式布局规则中会指定一个标准宽度,并根据组件原始布局列数和标准宽度计算出组件标准宽度,进而根据当前屏幕宽度计算出适配后的组件列数,可用如下公式表达:

响应式适配列数(标准屏幕宽度下组件列数) = (当前屏幕宽度÷(标准屏幕宽度÷标准屏幕宽度下组件列数×scale))

其中,scale为组件放大参数,标准屏幕宽度下组件原宽度投放到iPad上会过小,可以通过scale参数进行适当放大。

image.png

对于组件宽度适配,响应式规则会先计算标准屏幕宽度下的组件列数并进行列数适配,再通过适配后的列数计算适配宽度:

响应式适配宽度(标准屏幕宽度下组件宽度) = (当前屏幕宽度 - 边距间距)÷响应式适配列数(标准屏幕宽度÷标准屏幕宽度下组件宽度)

image.png

在以上公式中调整标准屏幕宽度及组件放大scale即可得到适配效果较好的通用布局规则,经过设计同学在各种设备尺寸下的调整总结,当前优酷中使用的标准屏幕宽度为440dp,scale为1.2倍,适配效果最佳。组件适配逻辑已在响应式SDK布局规则中统一实现,业务方直接调用即可,也方便设计同学对整个App的组件适配进行统一调整。

响应式SDK中YKRLCompLayoutManager类封装了相关布局逻辑,业务方也可通过YKRLCompLayoutAdapterProtocol协议二次处理,以定制响应式布局逻辑,在App统一架构中直接调用YKRLCompLayoutManager的相关接口即可获取按照响应式规则计算后的布局参数,如列数、宽度等,当监听响应式状态发生变化时重新布局即可完成响应式布局。

image.png

响应式数据处理
响应式数据处理包括数据映射、数据过滤、数据合并、数据补齐,数据处理逻辑两端一致,详细介绍可以参见:一个APP如何适配多个Android终端?,下面简单介绍一下iOS响应式数据映射的实现。

有些组件无法通过规则适配不同的屏幕尺寸,比如在手机上占整个屏幕宽度的组件(下图左侧带视频播放预约组件),如果采用等比放大的适配规则,在iPad端会显得过大,此类组件可以映射成相对简单的组件,以适配不同的屏幕尺寸。

image.png

优酷采用了统一抽象的数据结构,在组件映射方面比较容易实现,只需修改对应的组件标志即可。得益于统一架构的普遍推广和使用,我们在统一架构内添加了组件映射能力,方便各业务方调用,响应式SDK中提供了数据裁剪映射规则,业务方可以查询、增加相应的裁剪映射规则。对于未接入统一架构的业务方则需要业务方实现相关数据处理。

3 响应式业务流程

优酷响应式业务流程两端一致,响应式布局需要进行数据处理、响应式状态管理、触发布局等工作,优酷响应式SDK会在接口返回后处理相关数据,为统一架构提供相应布局接口,监控屏幕尺寸变化并触发布局等。

image.png

4 优酷响应式方案落地

iOS开发中经常采用绝对布局,而实现响应式的主要工作是将“绝对布局”修改为“相对布局”,接入工作较安卓更为繁琐。

image.png

iOS响应式可以按Window->ViewController->容器->组件的层级完成接入。

Window在配置支持分屏后会由系统自动布局,在RootViewController树中的子ViewController也会随Window自动布局,而特殊ViewController,如多tab页面的子ViewController等,未加入RootViewController树,需要手动修改为相对布局,页面可通过Autoresizing或监听响应式状态实现相对布局。

image.png

接入统一架构的页面容器由统一架构提供,统一架构容器的布局列数管理、布局宽度管理等都已接入响应式SDK,为业务方接入减少了大量工作,业务方只需指定自身所采用的布局规则即可,ViewController和容器实现相对布局后,每当屏幕尺寸变化时响应式SDK会通知容器重新布局,变换组件列数或宽度等,组件卡片只需要按容器提供的尺寸进行布局即可。

组件卡片内一般使用Frame绝对布局,需要修改为相对布局,简单的布局逻辑可以使用Autoresizing实现,方便快捷,复杂的布局可以使用AutoLayout或Masonry等自动布局框架(性能较差)实现,也可以在layoutSubviews方法中重新计算布局,业务方可以选择合适的方式实现自动布局,以减少接入成本。

对于未接入统一架构的页面则需要在本页面布局逻辑中手动接入响应式SDK相关布局接口。

image.png

落地过程中发现许多组件卡片布局时依赖了屏幕宽度,不符合响应式开发规范,导致适配响应式时工作量较大。每一层View只应依赖父层View布局,各层View实现相对布局后,每当屏幕尺寸改变时各层View会自动适配,同时容器的组件列数和尺寸会按响应式规则进行适配,一套代码即可适配所有屏幕尺寸,实现响应式布局。

三 优酷响应式成果

目前优酷全端已具备响应式布局的能力,八月份已上线universal版本,一套代码支持iPhone、iPad竖屏、iPad横屏、浮窗、各种比例分屏,为用户提供了更好更丰富的用户体验。

image.png

image.png

四 总结

响应式能力是多端投放能力的第一步,优酷实现响应式布局后对开发、设计和产品都提出了更高的要求,同时鉴于iPad低端设备占比较高,业务开发过程中不仅要考虑通投能力,更要求App始终保持更高的性能和稳定性,这是我们持续在努力的。

苹果2020年底将推出基于ARM架构的MacBook,也有媒体曝光,苹果正在申请折叠屏相关的专利,相信未来苹果设备的尺寸会越来越丰富,App适配提效是绕不开的话题,而优酷响应式的开发极大扩展了iPhone版App的适用场景,是解决多种设备支持的更好途径,为适应未来更复杂的设备场景打下坚实基础。

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

思否独立开发者丨@向前兄:编程在一定程度上也是认识这个世界的一种方式

微信读书笔记导出插件“小悦记”.png

独立项目名称:微信读书笔记导出插件“小悦记”

思否社区ID:@我是菜鸟


@向前兄来自河南洛阳,最近几年在上海工作,目前(new Date())是前端开发一个。

微信读书笔记导出插件“小悦记”

2016年,在大学毕业不到一年后,他只身一人来到了上海,用着jQuery+Bootstrap做起了响应式官网,开启了坎坷的工作历程。后来又跳入了React Native的 “坑”,一个人摸索着把APP上了架。一时兴起,又做了一个前端单词的小程序。后来限于公司前端不受重视,又开始了新的探索。

眨眼间,忙忙碌碌又两年,也算是经历了一番。不知所得可值得?不知所失可拥有?

对他来说,编程在一定程度上也是认识这个世界的一种方式。

长路漫漫,踏歌而行。

众所周知,微信读书App 是一款非常优秀的阅读类App ,周围也有不少人在用。虽然工作比较忙。但是也没少在上面看书做笔记。

美中不足的是,目前微信读书虽然支持笔记导出,但是提供的是将笔记复制到剪切板,然后由用户自行粘贴到其他地方的功能。

如果你的笔记比较多的话,需要分好几次才可以批量人工导出,每次选择还得记住上一次在什么位置,非常不方便。粘贴出去的格式,也因软件的不同而千差万别。

如下图所示:选择的笔记内容超过了系统剪切板上线。请筛选后重试

微信读书笔记导出插件“小悦记”.png

@向前兄 时常感到非常不方便,于是,就顺手开发了“小悦记”这个可以导出多种模式的Chrome 插件。

他说自己目前并不是全职的独立开发者,主要是想解决下实际生活中遇到的问题(学而时习之,不亦说乎),锻炼一下自己各方面的能力,为以后做准备。

独立开发项目小悦记

立项时间:2020年1月10日前

项目背景:去年用微信读书看书的时候发现如果笔记过长的话,会有“选择的笔记内容超过了系统剪切板上线。请筛选后重试”的提示,多次复制粘贴在移动端很不方便。

本身也不太习惯用手机,后来发现微信读书网页版上线了,还可以直接查看读书笔记,于是就有了这个想法

做这个插件主要是解决手机系统的笔记剪切限制,另外就是看到微信笔记复制的内容在印象笔记的格式比较好看,然后想优化一下导出的笔记格式,纯文本的不是太好看。

面向群体:为了确保不是就我一个人遇到这个问题,做之前我在网上搜了下,确实也有人有类似的需求。

1、如何做的第一版产品?

刚开始起名字也比较费脑,毕竟logo之类的也要和插件名字或者读书笔记导出功能相关,太小众的话也比较难记,直接取名微信读书之类的又担心侵犯权利,就围绕着“阅读”,“笔记”这几个词在想,然后取名“小悦记”。

logo设计也是比较考验人的,本来打算是一本书的形象或者直接用 font awesome字体,然后发现没太合适的,而且和别的app重合度也比较高。

logo设计,付费的话,自己也承担不起,毕竟开发这个就是在用爱发电。

后来自己根据“悦”字联想,刚好左边的竖心旁可以当做笔,右边是兑换的兑,然后竖心旁的两个点“心”上面的两个点,我本来打算用手绘的方式,但是没有找到合适的工具,时间比较急。

虽然之前切图经常用Photoshop,但是基本上只会使用切图、像章工具,之前做的微信小程序“前端单词”的logo也是用PPT做的,这次的logo也不例外。

功能方面的话就自己试验,自己写自己测试。

2、独立开发过程中遇到过哪些困难?最难搞定的是什么?

好几年没有用jQuery了,刚开始都有点不会用了。

还有就是以往没有开发过Chrome浏览器插件,不是太了解里面的运行机制。去网上找的资料也都比较旧了,复制粘贴的一大堆,官方虽然有教程,但是似乎偏理论多些。

后来做出来之后,想转成火狐浏览器插件,但是没有通过,这个比较纳闷,我去网上找了个开源的插件库,对方的也没有成功转为Firefox 插件,后来我就没有再考虑Firefox浏览器了。时间不太够,基本上是周六周日空了看下代码。

比较难搞定的基本上是自己能力范围之外的东西,在这上面花费时间比较多,本来打算是在读书日前发布的,结果晚了好几天。

提交审核需要付费,还是找的朋友帮忙的。

刚开始的推广可能被官方注意到了?然后没过多久就有人反馈微信读书主页会有提示,并且插件不能用。我当时比较好奇他们是怎么检测出来的 ,搜出来的方法并不可取,后来我终于想出来 了,改完后发现社区有个人也提示了下,不过我没及时看到。

第二次提交审核不知道为什么没有通过(Chrome已经有266个用户),考虑到很多用户并非程序员,可能无法科学上网,就直接提交到360浏览器了。前段看到社区有人下载代码后在QQ浏览器上直接运行了。

微信读书笔记导出插件“小悦记”.png

3、项目目前取得了哪些成就?项目为你带来了什么?

成就倒没有什么成就,就是确实解决了大家遇到的一个问题,新发现不少,就当做探索了吧。

首先是公众号涨了不少关注者,认识了不少人。

其次是探索下推广方式带来的效果如何,意外发现还是比较多的,就当是试验了。

认识一个00后,发现大学生接触到的信息来源和我们那时候几乎完全不同(知道善用佳软和小众软件的估计都毕业好些年了)。如果有新的产品推广,可能要考虑受众群体和实际情况了。

中间有在知乎大V群发个红包,但是刚开始效果好像并不明显,后来陆陆续续有人点赞和收藏。

在阮一峰老师的科技周刊投稿,获得了一次曝光的机会。

最后感谢朋友圈各位朋友的转发和打赏。

4、你的商业模式是什么?是如何增长的?

目前没有商业模式,只是初步尝试,所以只放了个人网站和公众号的链接。

5、近阶段项目有哪些更新,未来会做什么变动?

暂时没有更新的打算,它已经初步完成了它 的历史使命。目前在考虑另外一个工具,也是来自实际遇到的问题,产品需求已经列了二十多条了,不过可能得到明年有空了才能开始。

6、如果项目重来一次你会做哪些改变?

首先可能会按照规定时间节点开发,其次是安排好推广渠道和方式,毕竟花时间做出来了,要把效果发挥到最大。一开始还设想了短视频的方式,不过精力有限,最后只是在公众号用图文的形式推广了下。

还有就是,投入更多精力,增加更多功能吧,其实在这之前也有有类似的产品的,不过切入点不一样。

个人相关问题

1、推荐你最喜欢的一款产品 / 游戏 / App?并说明原因

平常不玩游戏,也没有太高频使用的产品,手机还设置了限制时长。坐地铁经常看 Inoreader、还有几个读书APP。平常用电脑多,比较经常网上逛。

2、分享一下你的技术栈和你日常的工作流?

技术栈的话,工作中用到的是 JavaScript(ES6+)、React、React-Native、Mobx、SCSS、Taro、小程序等。业余时间学点Node和偏后端的东西。

image.png

3、对独立开发者或编程初学者有什么建议?

以前在网上看到一句话说,如果深入一个细分领域的话,还是有机会的,后来发现,中国人实在太多了,一个你觉得已经很细分的地方其实都有不少人在做了。

之前有在一个开发者群众看到说,国内用户可能目前还是习惯白嫖,付费意识不是太强。

做一个成功且优秀的产品需要很多的能力,编码只是其中一少部分。

前段不是有个新闻吗,82岁学编程的老奶奶。想学编程永远不晚,现在网络比较发达,各种资料都很丰富,也不用在意自己的职业是什么。

4、生活中有什么爱好?有什么个人的特别的工作习惯么?

爱好不算太多,空了看书、上网,闲了会打羽毛球,不过已经是好久以前的事了。

最近几个月刚把加班加肥的又通过跑步减肥10斤。

特别的工作习惯?好像也没什么特别的,相对来说比较注重效率,另外个人还是比较喜欢安静点的工作环境。

5、你对国内技术社区的看法

非常感谢思否社区,我在上面还提了好几个问题,都有热心的程序员帮忙回答。也在上面关注了不少厉害的程序员。我感觉国内技术社区还是有很大的想象力的,如果运营得好的话,毕竟程序员群体还是很大的。其他技术社区一般也会看,不过个人感觉帮助更大的可能是自己平常发现的一些个人博客之类的(可能层面不一样)。


开发者寄语

能在这里打个广告吗?国庆后打算找工作, 上海地区有招前端的吗?感兴趣的话求带走 (base64)bGN5eGxxbkAxNjMuY29t


独立开发者支持计划-1.png

该内容栏目为「SFIDSP - 思否独立开发者支持计划」。为助力独立开发者营造更好的行业环境, SegmentFault 思否社区作为服务于开发者的技术社区,正式推出「思否独立开发者支持计划」,我们希望借助社区的资源为独立开发者提供相应的个人品牌、独立项目的曝光推介。

有意向的独立开发者或者独立项目负责人,可通过邮箱提供相应的信息(个人简介、独立项目简介、联系方式等),以便提升交流的效率。

联系邮箱:pr@segmentfault.com

image

二维码过期添加思否小姐姐拉你入群
image.png
查看原文

赞 6 收藏 1 评论 0

高阳Sunny 赞了文章 · 10月19日

ACM MM顶会论文 | 对话任务中的“语言-视觉”信息融合研究

目标导向的视觉对话是“视觉-语言”交叉领域中一个较新的任务,它要求机器能通过多轮对话完成视觉相关的特定目标。该任务兼具研究意义与应用价值。日前,北京邮电大学王小捷教授团队与美团AI平台NLP中心团队合作,在目标导向的视觉对话任务上的研究论文《Answer-Driven Visual State Estimator for Goal-Oriented Visual Dialogue-commentCZ》被国际多媒体领域顶级会议ACMMM 2020录用。

该论文分享了他们在目标导向视觉对话中的最新进展,即提出了一种响应驱动的视觉状态估计器(Answer-Driven Visual State Estimator,ADVSE)用于融合视觉对话中的对话历史信息和图片信息,其中的聚焦注意力机制(Answer-Driven Focusing Attention,ADFA)能有效强化响应信息,条件视觉信息融合机制(Conditional Visual Information Fusion,CVIF)用于自适应选择全局和差异信息。该估计器不仅可以用于生成问题,还可以用于回答问题。在视觉对话的国际公开数据集GuessWhat?!上的实验结果表明,该模型在问题生成和回答上都取得了当前的领先水平。

背景

一个好的视觉对话模型不仅需要理解来自视觉场景、自然语言对话两种模态的信息,还应遵循某种合理的策略,以尽快地实现目标。同时,目标导向的视觉对话任务具有较丰富的应用场景。例如智能助理、交互式拾取机器人,通过自然语言筛查大批量视觉媒体信息等。

图1 目标导向的视觉对话

研究现状及分析

为了进行目标导向的和视觉内容一致的对话,AI智能体应该能够学习到视觉信息敏感的多模态对话表示以及对话策略。对话策略学习的相关工作有很多,如Strub等人[1]首先提出使用强化学习来探索对话策略,随后的工作则着重于奖励设计[2,3]或动作选择[4,5]。但是,它们中的大多数采用了一种简单的方式来表示多模态对话,分别编码两个模态信息,即由RNN编码的语言特征和由预训练CNN编码的视觉特征,并将它们拼接起来。

好的多模态对话表示是策略学习的基石。为了改进多模态对话的表示,研究者们提出了各种注意机制[6,7,8],从而增强了多模态交互。尽管已有工作取得了许多进展,但是还存在一些重要问题。

  1. 在语言编码方面,现有方法的语言编码方式都不能对不同的响应(Answer)进行区分,Answer通常只是附在Question后面编码,由于Answer只是Yes或No一个单词,而Question则包含更长的词串,因此,Answer的作用很微弱。但实际上,Answer的回答很大程度决定了后续图像关注区域的变化方向,也决定了对话的发展方向,回答是Yes和No会导致完全不同的发展方向。例如图1中通过对话寻找目标物体的示例,当第一个问题的答案“是花瓶吗?”为“是”,则发问者继续关注花瓶,并询问可以最好地区分多个花瓶的特征;当第三个问题的答案“部分为红色吗?”为“否”,则发问者不再关注红色的花瓶,而是询问有关剩余候选物体的问题。
  2. 在视觉以及融合方面的情况也是类似,现有的视觉编码方式或者采用静态编码在对话过程中一直不变,直接和动态变化的语言编码拼接,或者用QA对编码引导对视觉内容的注意力机制。因此,也不能对不同的Answer进行有效区分。而如前所述,当Answer回答不同时,会导致图像关注区域产生非常不同的变化,一般地,当回答为“是”时,图像会聚焦于当前对象,进一步关注其特点,当回答为“否”时,可能需要再次关注图像整体区域去寻找新的可能候选对象。

响应驱动的视觉状态估计器

为此,本文提出一个响应驱动的视觉状态估计器,如下图2所示,新框架中包含响应驱动的注意力更新(ADFA-ASU)以及视觉信息的条件融合机制(CVIF)分别解决上述两个问题。

图2 响应驱动的视觉状态估计器框架图

响应驱动的注意力更新首先采用门限函数极化当前轮次Question引导的注意力,随后基于对该Question的不同Answer进行注意力反转或保持,得到当前Question-Answer对对话状态的影响,并累积到对话状态上,这种方式有效地强调了Answer对对话状态的影响;CVIF在当前QA的指导下融合图像的整体信息和当前候选对象的差异信息,从而获得估计的视觉状态。

答案驱动的注意力更新(ADFA-ASU)

视觉信息的条件融合机制(CVIF)

响应驱动的视觉状态估计器用于问题生成和回答

ADVSE是面向目标的视觉对话的通用框架。因此,我们将其应用于GuessWhat ?!中的问题生成(QGen)和回答(Guesser)建模。我们首先将ADVSE与经典的层级对话历史编码器结合起来以获得多模态对话表示,而后将多模态对话表示与解码器联合则可得到基于ADVSE的问题生成模型;将多模态对话表示与分类器联合则得到基于ADVSE的回答模型。

图3 响应驱动的视觉状态估计器用于问题生成和回答示意图

在视觉对话的国际公开数据集GuessWhat?!上的实验结果表明,该模型在问题生成和回答上都取得了当前的领先水平。我们首先给出了ADVSE-QGen和ADVSE-Guesser与最新模型对比的实验结果。

此外,我们评测了联合使用ADVSE-QGen和ADVSE-Guesser的性能。最后,我们给出了模型的定性分析内容。我们模型的代码即将可从ADVSE-GuessWhat获得。

表1 QGen任务性能对比,评测指标为任务成功率

表2 Guesser任务性能对比,评测指标为错误率

图4 问题生成过程中响应驱动的注意力转移样例分析

图5 ADVSE-QGen对话生成样例

总结

本论文提出了一种响应驱动的视觉状态估计器(ADVSE),以强调在目标导向的视觉对话中不同响应对视觉信息的重要影响。首先,我们通过响应驱动的集中注意力(ADFA)捕获响应对视觉注意力的影响,其中是保持还是移动与问题相关的视觉注意力由每个回合的不同响应决定。

此外,在视觉信息的条件融合机制(CVIF)中,我们为不同的QA状态提供了两种类型的视觉信息,然后依情况地将它们融合,作为视觉状态的估计。将提出的ADVSE应用于Guesswhat?!中的问题生成任务和猜测任务,与这两个任务的现有最新模型相比,我们可以获得更高的准确性和定性结果。后续,我们还将进一步探讨同时使用同源的ADVSE-QGen和ADVSE-Guesser的潜在改进。

参考文献

  • [1] FlorianStrub,HarmdeVries,JérémieMary,BilalPiot,AaronC.Courville,and Olivier Pietquin. 2017. End-to-end optimization of goal-driven and visually grounded dialogue systems. In Joint Conference on Artificial Intelligence.
  • [2] Pushkar Shukla, Carlos Elmadjian, Richika Sharan, Vivek Kulkarni, Matthew Turk, and William Yang Wang. 2019. What Should I Ask? Using Conversationally Informative Rewards for Goal-oriented Visual Dialog.. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics. Association for ComputationalLinguistics,Florence,Italy,6442–6451. https://doi.org/10.18653/v1/P...
  • [3] JunjieZhang,QiWu,ChunhuaShen,JianZhang,JianfengLu,andAntonvanden Hengel. 2018. Goal-Oriented Visual Question Generation via Intermediate Re- wards. In Proceedings of the European Conference on Computer Vision.
  • [4] Ehsan Abbasnejad, Qi Wu, Iman Abbasnejad, Javen Shi, and Anton van den Hengel. 2018. An Active Information Seeking Model for Goal-oriented Vision- and-Language Tasks. CoRR abs/1812.06398 (2018). arXiv:1812.06398 http://arxiv.org/abs/1812.06398
  • [5] EhsanAbbasnejad,QiWu,JavenShi,andAntonvandenHengel.2018.What’sto Know? Uncertainty as a Guide to Asking Goal-Oriented Questions. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 4150–4159.
  • [6] Chaorui Deng, Qi Wu, Qingyao Wu, Fuyuan Hu, Fan Lyu, and Mingkui Tan. 2018. Visual Grounding via Accumulated Attention. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 7746–7755.
  • [7] Tianhao Yang, Zheng-Jun Zha, and Hanwang Zhang. 2019. Making History Matter: History-Advantage Sequence Training for Visual Dialog. In Proceedings of the IEEE International Conference on Computer Vision. 2561–2569.
  • [8] BohanZhuang,QiWu,ChunhuaShen,IanD.Reid,andAntonvandenHengel. 2018. Parallel Attention: A Unified Framework for Visual Object Discovery Through Dialogs and Queries. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 4252–4261.

作者简介

本文作者包括徐子彭、冯方向、王小捷、杨玉树、江会星、王仲远等等,他们来自北京邮电大学人工智能学院智能科学与技术中心与美团搜索与NLP中心团队。

招聘信息

美团搜索与NLP部,长期招聘搜索、推荐、NLP算法工程师,坐标北京/上海。欢迎感兴趣的同学发送简历至:tech@meituan.com(邮件注明:搜索与NLP部)

想阅读更多技术文章,请关注美团技术团队(meituantech)官方微信公众号。

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

AIOps在美团的探索与实践——故障发现篇

一、背景

AIOps,最初的定义是Algorithm IT Operations,是利用运维算法来实现运维的自动化,最终走向无人化运维。随着技术成熟,逐步确定为Artificial Intelligence for IT Operations——智能运维,将人工智能应用于运维领域,基于已有的运维数据(日志、监控信息、应用信息等),通过机器学习的方式来进一步解决自动化运维无法解决的问题。

早期的运维工作大部分是由运维人员手工完成的,手工运维在互联网业务快速扩张、人力成本高企的时代,难以维系。于是,自动化运维应运而生,它主要通过可被自动触发、预定义规则的脚本,来执行常见、重复性的运维工作,从而减少人力成本,提高运维的效率。总的来说,自动化运维可以认为是一种基于行业领域知识和运维场景领域知识的专家系统。随着整个互联网业务急剧膨胀,以及服务类型的复杂多样,“基于人为指定规则”的专家系统逐渐变得力不从心,自动化运维的不足,日益凸显,当前美团在业务监控和运维层面也面临着同样的困境。

DevOps的出现,部分解决了上述问题,它强调从价值交付的全局视角,但DevOps更强调横向融合及打通,AIOps则是DevOps在运维(技术运营)侧的高阶实现,两者并不冲突。AIOps不依赖于人为指定规则,主张由机器学习算法自动地从海量运维数据(包括事件本身以及运维人员的人工处理日志)中不断地学习,不断提炼并总结规则。AIOps在自动化运维的基础上,增加了一个基于机器学习的大脑,指挥监测系统采集大脑决策所需的数据,做出分析、决策,并指挥自动化脚本去执行大脑的决策,从而达到运维系统的整体目标。综上看,自动化运维水平是AIOps的重要基石,而AIOps将基于自动化运维,将AI和运维很好地结合起来,这个过程需要三方面的知识:

  1. 行业、业务领域知识,跟业务特点相关的知识经验积累,熟悉生产实践中的难题。
  2. 运维领域知识,如指标监控、异常检测、故障发现、故障止损、成本优化、容量规划和性能调优等。
  3. 算法、机器学习知识,把实际问题转化为算法问题,常用算法包括如聚类、决策树、卷积神经网络等。

美团技术团队在行业、业务领域知识和运维领域的知识等方面有着长期的积累,已经沉淀出不少工具和产品,实现了自动化运维,同时在AIOps方面也有一些初步的成果。我们希望通过在AIOps上持续投入、迭代和钻研,将之前积累的行业、业务和运维领域的知识应用到AIOps中,从而能让AIOps为业务研发、产品和运营团队赋能,提高整个公司的生产效率。

二、技术路线规划

2.1 AIOps能力建设

AIOps的建设可以先由无到局部单点探索,在单点探索上得到初步的成果,再对单点能力进行完善,形成解决某个局部问题的运维AI学件,再由多个具有AI能力的单运维能力点组合成一个智能运维流程。行业通用的演进路线如下:

  1. 开始尝试应用AI能力,还无较为成熟的单点应用。
  2. 具备单场景的AI运维能力,可以初步形成供内部使用的学件。
  3. 有由多个单场景AI运维模块串联起来的流程化AI运维能力,可以对外提供可靠的运维AI学件。
  4. 主要运维场景均已实现流程化免干预AI运维能力,可以对外提供供可靠的AIOps服务。
  5. 有核心中枢AI,可以在成本、质量、效率间从容调整,达到业务不同生命周期对三个方面不同的指标要求,可实现多目标下的最优或按需最优。

所谓学件,亦称AI运维组件[1](南京大学周志华老师原创),类似程序中的API或公共库,但API及公共库不含具体业务数据,只是某种算法,而AI运维组件(或称学件),则是在类似API的基础上,兼具对某个运维场景智能化解决的“记忆”能力,将处理这个场景的智能规则保存在了这个组件中,学件(Learnware)= 模型(Model)+规约(Specification)。AIOps具体的能力框架如下图1所示:

图1 AIOps能力框架图

2.2 关联团队建设

AIOps团队内部人员根据职能可分为三类团队,分别为SRE团队、开发工程师(稳定性保障方向)团队和算法工程师团队,他们在AIOps相关工作中分别扮演不同的角色,三者缺一不可。SRE能从业务的技术运营中,提炼出智能化的需求点,在开发实施前能够考虑好需求方案,产品上线后能对产品数据进行持续的运营。开发工程师负责进行平台相关功能和模块的开发,以降低用户的使用门槛,提升用户的使用效率,根据企业AIOps程度和能力的不同,运维自动化平台开发和运维数据平台开发的权重不同,在工程落地上能够考虑好健壮性、鲁棒性、扩展性等,合理拆分任务,保障成果落地。算法工程师则针对来自于SRE的需求进行理解和梳理,对业界方案、相关论文、算法进行调研和尝试,完成最终算法落地方案的输出工作,并不断迭代优化。各团队之间的关系图如下图2所示:

图2 AIOps关联团队关系图

2.3 演进路线

当前,我们在质量保障方面的诉求最迫切,服务运维部先从故障管理领域探索AIOps实践。在故障管理体系中,从故障开始到结束主要有四大核心能力,即故障发现、告警触达、故障定位、故障恢复。故障发现包含了指标预测、异常检测和故障预测等方面,主要目标是能及时、准确地发现故障;告警触达包含了告警事件的收敛、聚合和抑制,主要目标是降噪聚合,减少干扰;故障定位包含了数据收集、根因分析、关联分析、智能分析等,主要目标是能及时、精准地定位故障根因;故障恢复部分包含了流量切换、预案、降级等,主要目标是及时恢复故障,减少业务损失,具体关系如下图3所示:

图3 故障管理体系核心能力关系图

其中在故障管理智能化的过程中,故障发现作为故障管理中最开始的一环,在当前海量指标场景下,自动发现故障和自动异常检测的需求甚为迫切,能极大地简化研发策略配置成本,提高告警的准确率,减少告警风暴和误告,从而提高研发的效率。除此之外,时序数据异常检测其实是基础能力,在后续告警触达、故障定位和故障恢复环节中,存在大量指标需要进行异常检测。所以将故障发现作为当前重点探索目标,解决当前海量数据场景下人工配置和运营告警策略、告警风暴和准确率不高的核心痛点。整个AIOps体系的探索和演进路线如下图4所示。每个环节均有独立的产品演进,故障发现-Horae(美团服务运维部与交易系统平台部共建项目)、告警触达-告警中心、故障定位-雷达、故障恢复-雷达预案。

图4 AIOps在故障管理方面的演进路线

三、AIOps之故障发现

3.1 故障发现

从美团现有的监控体系可以发现,绝大多数监控数据均为时序数据(Time Series),时序数据的监控在公司故障发现过程中扮演着不可忽视的角色。无论是基础监控CAT[2]、MT-Falcon[3]、Metrics(App端监控),还是业务监控Digger(外卖业务监控)、Radar(故障发现与定位平台)等,均基于时序数据进行异常监控,来判断当前业务是否在正常运行。然而从海量的时序数据指标中可以发现,指标种类繁多、关系复杂(如下图5所示)。在指标本身的特点上,有周期性、规律突刺、整体抬升和下降、低峰期等特点,在影响因素上,有节假日、临时活动、天气、疫情等因素。原有监控系统的固定阈值类监控策略想要覆盖上述种种场景,变得越来越困难,并且指标数量众多,在策略配置和优化运营上,人力成本将成倍增长。若在海量指标监控上,能根据指标自动适配合适的策略,不需要人为参与,将极大的减少SRE和研发同学在策略配置和运营上的时间成本,也可让SRE和研发人员把更多精力用在业务研发上,从而产生更多的业务价值,更好地服务于业务和用户。

图5 时序数据种类多样性

3.2 时序数据自动分类

在时序数据异常检测中,对于不同类型的时序数据,通常需要设置不同的告警规则。比如对于CPU Load曲线,往往波动剧烈,如果设置固定阈值,瞬时的高涨会经常产生误告,SRE和研发人员需要不断调整阈值和检测窗口来减少误告,当前,通过Radar(美团内部系统)监控系统提供的动态阈值策略,然后参考历史数据可以在一定程度上避免这一情况。如果系统能够提前预判该时序数据类型,给出合理的策略配置建议,就可以提升告警配置体验,甚至做到自动化配置。而且在异常检测中,时序数据分类通常也是智能化的第一步,只有实现智能化分类,才能自动适配相应的策略。

目前,时间序列分类主要有两种方法,无监督的聚类和基于监督学习的分类。Yading[4]是一种大规模的时序聚类方法,它采用PAA降维和基于密度聚类的方法实现快速聚类,有别于K-Means和K-Shape[5]采用互相关统计方法,它基于互相关的特性提出了一个新颖的计算簇心的方法,且在计算距离时尽量保留了时间序列的形状。对KPI进行聚类,也分为两种方法,一种是必须提前指定类别数目(如K-Means、K-Shape等)的方法,另一种是无需指定类别数目(如DBSCAN等),无需指定类别数目的聚类方法,类别划分的结果受模型参数和样本影响。至于监督学习的分类方法,经典的算法主要包括Logistics、SVM等。

3.2.1 分类器选择

根据当前监控系统中时序数据特点,以及业内的实践,我们将所有指标抽象成三种类别:周期型、平稳型和无规律波动型[6]。我们主要经历了三个阶段的探索,单分类器分类、多弱分类器集成决策分类和卷积神经网络分类。

  1. 单分类器分类:本文训练了SVM、DBSCAN、One-Class-SVM(S3VM)三种分类器,平均分类准确率达到80%左右,但无规律波动型指标的分类准确率只有50%左右,不满足使用要求。
  2. 多弱分类器集成决策分类:参考集成学习相关原理,通过对SVM、DBSCAN、S3VM三种分类器集成投票,提高分类准确率,最终分类准确率提高7个百分点,达到87%。
  3. 卷积神经网络分类:参考对Human Activity Recognition(HAR)进行分类的实践[7],我们用CNN(卷积神经网络)实现了一个分类器,该分类器在时序数据分类上表现优秀,准确率能达到95%以上。CNN在训练中会逐层学习时序数据的特征,不需要成本昂贵的特征工程,大大减少了特征设计的工作量。

3.2.2 分类流程

我们选择CNN分类器进行时序数据分类,分类过程如下图6所示,主要步骤如下:

图6 时序数据分类处理流程

  1. 缺失值填充:时序数据存在少量数据丢失或者部分时段无数据等现象,因此在分类前先对数据先进行缺失值填充。
  2. 标准化:本文采用方差标准化对时序数据进行处理。
  3. 降维处理:按分钟粒度的话,一天有1440个点,为了减少计算量,我们进行降维处理到144个点。PCA、PAA、SAX等一系列方法是常用的降维方法,此类方法在降低数据维度的同时还能最大程度地保持数据的特征。通过比较,PAA在降到同样的维度(144维)时,还能保留更多的时序数据细节,具体对比如下图7所示。
  4. 模型训练:使用标注的样本数据,在CNN分类器中进行训练,最终输出分类模型。

图7 PAA、SAX降维方法对比

3.3 周期型指标异常检测

3.3.1 异常检测方法

基于上述时序数据分类工作,本文能够相对准确地将时序数据分为周期型、平稳型和无规律波动型三类。在这三种类型中,周期型最为常见,占比30%以上,并且包含了大多数业务指标,业务请求量、订单数等核心指标均为周期型,所以本文优先选择周期型指标进行自动异常检测的探索。对于大量的时序数据,通过规则进行判断已经不能满足,需要通用的解决方案,能对所有周期型指标进行异常检测,而非一个指标一套完全独立的策略,机器学习方法是首选。

论文Opprentice[8]和腾讯开源的Metis[9]采用监督学习的方式进行异常检测,其做法如下:首先,进行样本标注得到样本数据集,然后进行特征提取得到特征数据集,使用特征数据集在指定的学习系统上进行训练,得到异常分类模型,最后把模型用于实时检测。监督学习整体思路[10]如下图8所示,其中
(x1,y1),(x2,y2),...,(xn,yn)是训练数据集,学习系统由训练数据学习一个分类器P(Y∣X)或Y=f(X),分类系统通过学习到的分类器对新的输入实例xn+1进行分类,预测其输出的类别yn+1。

图8 监督学习在分类问题中的应用

3.3.2 异常注入

一般而言,在样本数据集中,正负样本比例如果极度不均衡(比如1:5,或者更悬殊),那么分类器分类时就会倾向于高比例的那一类样本(假如负样本占较大比例,则会表现为负样本Recall过高,正样本Recall低,而整体的Accuracy依然会有比较好的表现),在一个极度不均衡的样本集中,由于机器学习会对每个数据进行学习,那么多数数据样本带有的信息量就比少数样本信息量大,会对分类器学习过程中造成干扰,导致分类不准确。

在实际生产环境中,时序数据异常点是非常少见的,99%以上的数据都是正常的。如果使用真实生产环境的数据进行样本标注,将会导致正负样本比例严重失衡,导致精召率无法满足要求。为了解决基于监督学习的异常检测异常点过少的问题,本文设计一种针对周期型指标的自动异常注入算法,保证异常注入足够随机且包含各种异常场景。

时序数据的异常分为两种基本类型,异常上涨和异常下跌,如下图9(图中数据使用Curve[11]标注),通常异常会持续一段时间,然后逐步恢复,恢复过程或快或慢,影响异常两侧的值,称之为涟漪效应(Ripple Effect),类似石头落入水中,波纹扩散的情形。受到该场景的启发,异常注入思路及步骤如下:

图9 异常case中异常数据分布

  1. 给定一段时序值S,确定注入的异常个数N,将时序数据划分为N块。
  2. 在其中的一个区域X中,随机选定一个点Xi作为异常种子点。
  3. 设定异常点数目范围,基于此范围产生随机出异常点数n,异常点随机分布在异常种子两侧,左侧和右侧的数目随机产生。
  4. 对于具体的异常点,根据其所在位置,选择该点邻域范围数据作为参考数据集m,需要邻域在设定的范围内随机产生。
  5. 产生一个随机数,若为奇数,则为上涨,否则下跌。基于参考数据集m,根据3Sigma原理,生成超出±3σ的数据作为异常值。
  6. 设定一个影响范围,在设定范围内随机产生影响的范围大小,左右两侧的影响范围也随机分配,同时随机产生异常衰减的方式,包括简单移动平均、加权移动平均、指数加权移动平均三种方式。
  7. 上述过程只涉及突增突降场景,而对于同时存在升降的场景,通过分别生成上涨和下跌的上述两个异常,然后叠加在一起即可。

通过上面的异常注入步骤,能比较好地模拟出周期型指标在生产环境中的各种异常场景,上述过程中各个步骤的数据都是随机产生,所以产生的异常案例各不相同,从而能为我们生产出足够多的异常样本。为了保证样本集的高准确性,我们对于注入异常后的指标数据还会进行标注,以去除部分注入的非异常数据。具体异常数据生成效果如图10所示,其中蓝色线为原始数据,红色线为注入的异常,可以看出注入异常与线上环境发生故障时相似,注入的异常随机性较大。

图10 异常注入效果图

3.3.3 特征工程

针对周期型指标,经标注产生样本数据集后,需要设计特征提取器进行特征提取,Opprentice中设计的几种特征提取器如图11所示:

图11 论文Opprentice特征提取器

上述特征主要是一些简单的检测器,包括如固定阈值、差分、移动平均、SVD分解等。Metis将其分为三种特征,一是统计特征,包括方差、均值、偏度等统计学特征;二是拟合特征,包括如移动平均、指数加权移动平均等特征;三是分类特征,包含一些自相关性、互相关性等特征。参考上述提及的特征提取方法,本文设计了一套特征工程,区别于上述特征提取方法,本文对提取的结果用孤立森林进行了一层特征抽象,使得模型的泛化能力更强,所选择的特征及说明如下图12所示:

图12 特征选择及说明

3.3.4 模型训练及实时检测

参考监督学习在分类问题中的应用思路,对周期型指标自动异常检测方案具体设计如图下13所示,主要分为离线模型训练和实时检测两大部分,模型训练主要根据样本数据集训练生成分类模型,实时检测利用分类模型进行实时异常检测。具体过程说明如下:

  1. 离线模型训练:基于标注的样本数据集,使用设计的特征提取器进行特征提取,生成特征数据集,通过Xgboost进行训练,得到分类模型,并存储。
  2. 实时检测:线上实时检测时,时序数据先经过预检测(降低进入特征提取环节概率,减少计算压力),然后根据设计的特征工程进行特征提取,再加载离线训练好的模型,进行异常分类。
  3. 数据反馈:如果判定为异常,将发出告警。进一步地,用户可根据实际情况对告警进行反馈,反馈结果将加入样本数据集中,用于定时更新检测模型。

图13 模型检测和实时检测流说明

3.3.5 特殊场景优化

通过上述实践,本文得到一套可完整运行的周期型指标异常检测系统,在该系统应用到生产环境的过程中,也遇到不少问题,比如低峰期(小数值)波动幅度较大,节假日和周末趋势和工作日趋势完全不同,数据存在整体大幅抬升或下降,部分规律波动时间轴上存在偏移,这些情况都有可能产生误告。本文也针对这些场景,分别提出对应的优化策略,从而减少周期型指标在这些场景下的误告,提高异常检测的精召率。

1)低峰期场景:低峰期主要表现是小数值高波动,低峰期的波动比较普遍,但是常规检测时,只获取当前点前后7min的邻域内的数据,可能无法获取到本身已经出现过多次的较大波动,导致误判为异常。所以对于低峰期,需要扩大比较窗口,容纳到更多的正常的较大波动场景,从而减少被误判。如下图14所示,红色是当日数据,灰色是上周同日数据,如果判断窗口为w1,w1内蓝色点有可能被认为是异常点,而时间窗口范围扩大到w2后,大幅波动的蓝色点和绿色点都会被捕获到,出现类似大幅波动时不再被判定为异常,至于低峰期范围可以通过历史数据计算进行识别。

图14 低峰期时不同时段的相似大幅波动

2)节假日场景:节假日前一天以及节假日之后一周的数据,和正常周期的趋势都会有较大差别,可能会出现误告。本文通过提前配置需要进行节假日检测的日期,在设置的日期范围内,除了进行正常的检测流程,对于已经检测出异常的数据点,会再进入到节假日检测流程,都异常才会触发告警。节假日检测会取最近1h的数据,分别计算其波动比、周同比、日环比等数据,当前时间的这些指标通过“孤立森林”判断都为异常,才会认为数据是真正异常。除此之外,对于节假日,模型的敏感度会适当调低以适应节假日场景。

3)整体抬升/下降场景:场景特点如下图15所示,在该场景下,会设置一个抬升/下跌率,比如80%,如果今天最近1h数据80%相对昨日和上周都上涨,则认为是整体抬升,都下跌则认为是整体下降。如果出现整体抬升情况,会降低模型敏感度,并且要求当前日环比、周同比在1h数据中均为异常点,才会判定当前的数据异常。

图15 整体抬升下降场景

4)规律波动偏移场景:部分指标存在周期性波动,但是时间上会有所偏移,如图16所示案例中时序数据由于波动时间偏移导致误告。本文设计一种相似序列识别算法,在历史数据中找出波动相似的序列,如果存在足够多的相似波动序列,则认为该波动为正常波动。相似序列提取过程如下:最近n分钟的时序作为基础序列x,获取检测时刻历史14天邻域内的数据(如前后30min),在邻域数据中指定滑动窗口(如3min)滑动,把邻域数据分为多个长度为n的序列集Y,计算基础序列x与Y中每个序列的DTW距离,通过“孤立森林”对距离序列进行异常判断,对于被判定为异常值的DTW距离,它所对应的序列将被视为相似序列。如果相似序列个数超出设定阈值,则认为当前波动为规律偏移波动,属于正常现象。根据上述方法,提取到对应的相似序列如图16右边所示,其中红实线为基础序列。

图16 规律波动偏移相似序列提取

3.4 异常检测能力平台化

为了把上述时序数据异常检测探索的结果进行落地,服务运维部与交易系统平台部设计和开发了时序数据异常检测系统Horae。Horae致力于时间序列异常检测流程的编排与调优,处理对象是时序数据,输出是检测流程和检测结果,核心算法是异常检测算法、时间序列预测算法以及针对时间序列的特征提取算法。除此之外,Horae还会针对特殊的场景开发异常检测算法。Horae核心能力是可根据提供的算法,编排不同的检测流程,对指标进行自动分类,并针对指标所属类型自动选择合适的检测流程,进行流程调优得到该指标下的最优参数,从而确保能适配指标并得到更高的精召率,为各个对时序数据异常检测有需求的团队提供高准确率的异常检测服务。

3.4.1 Horae系统架构设计

Horae系统由四个模块组成:数据接入、实时检测、实验模块和算法模块。用户通过数据接入模块注册需要监听时序数据的消息队列,Horae系统将监听注册的Topic采集时序数据,并根据粒度(例如分钟、小时或天)更新每个时间序列,每个时序点都存储到时序数据库中,实时检测模块对每个时序点执行异常检测,当检测到异常时,通过消息队列将异常信息传输给用户。下图17详细展示了Horae系统的整体架构图。

  1. 数据接入:用户可以通过创建数据源用于数据上报,数据源可以包含一个或多个指标,指标更新频率最小为一分钟。不同数据源中指标的时序数据相互隔离,时序数据更新到使用Elasticsearch改造后的时序数据库中。
  2. 实时检测:采集到实时的时序数据后,会根据指标绑定的执行流程立即进行异常检测,如果没有训练调优,会先执行训练调优以保证更佳的精召率。实时检测的结果会通过消息队列通知到用户,用户根据异常检测的结果进行进一步处理判断是否需要发出告警。
  3. 实验模块:该模块主要功能是样本管理、算法注册、流程编排、模型训练和评估、模型发布。该模块提供样本管理功能,可对样本进行标注和存储;对于已经实现的算法,可以注册到系统中,供流程编排使用;通过算法编排得到的流程,可以用在模型训练或者异常检测中;训练流程会使用到标注好的样本数据调用算法离线服务进行离线训练并存储模型;对于已经编排好的检测流程,可以对指标进行模拟观察检测异常检测效果,或者离线回归判断检测流程在该指标上的具体精召率数据。
  4. 算法模块:算法模块提供了所有在实验模块注册的异常检测算法的具体实现,算法模块既可以执行单个算法,也可以接受多个算法编排的流程进行执行。当前支持的算法大类主要有预处理算法(异常值去除、空值填充、降维、归一化等),时序特征算法(统计类特征、拟合特征、分类特征等),机器学习类算法(RF、SVM、XGBoost、GRU、LSTM、CNN、聚类算法等),检测类算法(孤立森林、LOF、SVM、3Sigma、四分位、IQR等),预测类算法(Ewma、Linear Weighted MA、Holt-Winters、STL、SAIMAX、Prophet等),自定义算法(形变分析、同环比、波动比等)。

图17 Horae时序数据异常检测系统架构图

3.4.2 算法注册和模型编排

算法模型是对算法的抽象,通过唯一字符串标识算法模型,注册算法时需要指定算法的类型、接口、参数、返回值和处理单个时序点所需要加载的时序数据配置。成功注册的算法模型根据算法类型的不同,会生成用于模型编排的算法组件或对异常检测模型进行训练的组件。用于模型编排的算法组件主要包括:预处理算法、时序特征算法、评估算法、预测算法、分类算法、异常检测算法等,用于模型训练的算法分为两大类:参数调优和机器学习模型训练。

注册后的算法模型通常不会直接用于异常检测,会对算法模型进行编排后得到一个流程模型,流程模型可以用于执行异常检测或者执行训练。实验模块支持两种类型的流程模型:执行流程和训练流程。执行流程是一个异常检测流程,指定指标和检测时间段,得到检测时间段每个时序点的异常分值;训练流程是一个执行训练的流程模型,主要包括参数调优训练流程和机器学习模型训练流程。使用算法进行流程编排如下图18所示,左侧菜单为算法组件,中间区域可对算法执行流程进行编排调整,右侧区域是具体算法的参数设置。

图18 流程编排示意图

3.4.3 离线训练和实时检测

在模型编排阶段,可编排执行流程和训练流程,执行流程主要用在指标实时异常检测过程,而训练流程主要用在离线模型训练和参数调优。执行流程由流程配置和异常分值配置构成,由实验模块的流程调度引擎负责执行调度,下图19展示了执行流程的详细构成。流程调度引擎在对执行流程调度执行之前,会从流程的最深叶子节点的算法组件开始递归计算需要加载的时序数据集,根据流程中算法组件的参数配置,加载前置训练流程的训练结果,最后对流程中的算法组件依次调度执行,得到检测时间段每个时序点的异常分值。最终实现后的执行流程编排如图18所示。

图19 执行流程组成和处理过程

训练流程由流程配置、训练算法、样本加载配置和训练频次等信息构成,由实验模块的流程调度引擎负责调度执行,下图20展示了训练流程的详细构成。训练流程主要分为两大类,参数调优训练和机器学习模型训练。参数调优训练是指为需要调优的参数设置参数值迭代范围或者枚举值,通过贝叶斯调优算法对参数进行调优,得到最优参数组合;机器学习模型训练则通过设计好特征工程,设置分类器和超参数范围后调优得到机器学习模型文件。训练流程执行训练的样本集来源于人工标注的样本或者基于自动样本构造功能生成的样本。训练流程编排具体过程和执行流程类似,不同的是训练流程可设置定时任务执行,训练的结果会存储供后续使用。

图20 训练流程组成和处理过程

异常检测模型中会包含很多凭借经验设定的超参数,不同的指标可能需要设置不同的参数值,保证更高的精召率。而指标数据会随着时间发生变化,设置参数需要定期的训练和更新,在实验模块中可以为训练流程设置定时任务,实验模块会定时调度训练流程生成离线训练任务,训练任务执行完成可以看到训练结果和效果。下图21示例展示了一个参数调优训练流程的示例。

图21 参数调优训练结果示例

3.4.4 模型案例和结果评估

根据在周期型指标上探索的结果,在Horae上编排分类模型训练流程,训练和测试所使用的样本数是28000个,其中用于训练的比例是75%,用于验证的比例是25%,具体分类模型训练结果如下图22所示,在测试集上的准确率94%,召回率89%。同时编排了与之对应的执行流程,它的检测流程除了异常分类,还主要包含了空值填充、预检测、特征提取、分类判断、低峰期判断、偏移波动判断等逻辑,该执行流程适用范围是周期型和稳定型指标。除此之外,还提供了流程调优能力,检测流程中的每个算法可以暴露其超参数,对于具体的指标,通过该指标的样本数据可以训练得到该流程下的一组较优超参数,从而提高该指标的异常检测的精召率。

图22 异常分类模型训练结果

该异常检测流程应用到生产环境的指标后,具体检测效果相关案例如下图23所示,对于周期型指标,能及时准确地发现异常,对异常点进行反馈,准确率达到90%以上。除此之外,还对比了形变分析异常检测,对于生产环境中遇到的三个形变分析无法发现的4个案例,周期型指标异常检测流程能发现其中3个,表现优于形变分析。

图23 周期型指标异常检测模型生产环境检测结果

四、总结与展望

时序数据异常检测作为AIOps中故障发现环节的核心,当前经过探索和实践,已经在周期型指标异常检测上取得了一定的成绩,并落地到Horae时序异常检测系统中。在时序数据异常检测部分,后续会陆续实现平稳型、无规律波动型指标自动异常检测,增加指标数据预测相关能力,提高检测性能,从而实现所有类型的海量指标自动异常检测的目的。

除此之外,在告警触达方面,我们当前在进行告警收敛、降噪和抑制相关的规则和算法的探索,致力于提供精简有效的信息,减少告警风暴及干扰;在故障定位方面,我们已经基于规则在定位上取得比较不错的效果,后续还会进行更全面的定位场景覆盖和关联性分析、根因分析、知识图谱相关的探索,通过算法和规则提升故障定位的精召率。因篇幅所限,告警触达(告警中心)和故障定位(雷达)两部分内容将会在后续的文章中详细进行分享,敬请期待。

五、参考资料

  • [1] 周志华. 机器学习: 发展与未来[R]. 报告地: 深圳, 2016.
  • [2] 美团实时监控系统CAT[EB/OL]. https://tech.meituan.com/CAT_... 2018-11-01.
  • [3] 美团系统指标监控Mt-Falcon[EB/OL]. https://tech.meituan.com/Mt-F... 2017-02-24.
  • [4] Ding R, Wang Q, Dang Y, et al. Yading: fast clustering of large-scale time series data[J]. Proceedings of the VLDB Endowment, 2015, 8(5): 473-484.
  • [5] Paparrizos J, Gravano L. k-shape: Efficient and accurate clustering of time series[C]. Proceedings of the 2015 ACM SIGMOD International Conference on Management of Data. ACM, 2015: 1855-1870.
  • [6] H. Ren, Q. Zhang, B. Xu, Y. Wang, C. Yi, C. Huang, X. Kou, T. Xing, M. Yang, and J. Tong, “Time-series anomaly detection serviceat microsoft,” in Proceedings of the ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, pp. 3009–3017, ACM, Jun. 2019.
  • [7] Tom Brander. Time series classification with Tensorflow[EB/OL]. https://burakhimmetoglu.com/2... 2017-08-22.
  • [8] Liu D, Zhao Y, Xu H, et al. Opprentice: Towards practical and automatic anomaly detection through machine learning[C]//Proceedings of the 2015 Internet Measurement Conference. ACM, 2015: 211-224.
  • [9] Metis is a learnware platform in the field of AIOps[EB/OL]. https://github.com/Tencent/Me... 2018-10-12.
  • [10] 李航. 统计学习方法 [M]. 第2版. 北京: 清华大学出版社, 2019.28-29.
  • [11] An tool to help label anomalies on time-series data[EB/OL]. https://github.com/baidu/Curve, 2018-08-07.

六、作者简介

胡原、锦冬、俊峰,基础技术部-服务运维部工程师;长伟、永强,到家事业群-交易系统平台部工程师。

招聘信息

基础技术部-服务运维部-运维工具开发组-故障管理开发组主要负责故障发现、故障定位、故障恢复、故障运营、告警中心、风险管理、数据仓库等工作。目前团队诚招高级工程师、技术专家。欢迎有兴趣的同学投送简历至tech@meituan.com(邮件主题注明:运维工具)

想阅读更多技术文章,请关注美团技术团队(meituantech)官方微信公众号。

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

通过编译器插件实现代码注入

原文:通过编译器插件实现代码注入 | AlloyTeam
作者:林大妈

背景问题

大型的前端系统一般是模块化的。每当发现问题时,模块负责人总是要重复地在浏览器中找出对应的模块,略读代码后在对应的函数内打上断点,最终开始排查。

大部分情况下,我们会选择在固定的位置(例如模块的入口,或是类的构造函数)打上断点。也就意味着打断点的过程对于开发者来说是机械的劳动。那么有没有办法在不污染源代码的基础上通过配置来为代码打上断点呢?

实现思路

要想不污染源代码,只能选择在编译时进行处理,才能将想要的内容注入到目标代码中。代码编译的基本原理是将源代码处理成单词串,再将单词串组织成抽象语法树,最终再通过遍历抽象语法树并转换上面的节点而形成目标代码。

因此,代码注入的关键点就在于在抽象语法树形成时对语法树节点进行处理。前端代码通常会使用 babel 进行编译。

熟悉 babel 的基本原理

babel 的组成

babel 的核心是 babel-core。babel-core 可被划分成三个部分,分别处理对应的三个编译过程:

  1. babel-parser —— 负责将源代码字符串“单词化”并转化成抽象语法树
  2. babel-traverse —— 负责遍历抽象语法树并附加处理
  3. babel-generator —— 负责通过抽象语法树生成目标代码

babel-parser

整个 babel-parser 使用继承的方式,根据功能的不同逐层包装:

tokenizer

babel-parser 的一个核心是“tokenizer”,可以理解为“单词生成器”。babel 维护了一个 state(一个全局的状态),它会通过记录一些元信息提供给编译器,例如:

  • “这段 JavaScript 代码是否使用了严格模式?”
  • “我们现在识别到第几行第几列了?”
  • “这段代码里有哪些注释?”

tokenizer 的内部定义了不同的方法以识别不同的内容,例如:

  • 读到引号时通过 readString 方法尝试生成一个字符串 token
  • 读到数字时通过 readNumber 方法尝试生成一个数字 token

LVal/Expression/StatementParser

babel-parser 的另一个核心是“parser”,可以理解为“语法树生成器”。其中,StatementParser 是子类,当我们引入 babel-parser 并调用 parse 方法时,识别过程将从此处启动(babel 应该是将整个文件认为是一个语句节点)。

同样地,这些 parser 的内部也都为识别不同的内容而定义不同的方法,例如:

  • 识别到 true 时,生成一个 Boolean 字面量表达式
  • 识别到 @ 这个符号时,生成一个  装饰器语句节点

babel-traverse

babel-traverse 提供方法遍历语法树。它使用访问者模式,为外界提供添加遍历时附加操作的入口。

在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。

TraversalContext

遍历语法树时,babel 同样定义了一个 context 判断是否需要遍历以及遍历的方式。

TraversalContext 先将节点和外来的访问者进行有效化的处理,然后构造访问队列,最后启动深度优先遍历整棵语法树的过程。

class TraversalContext {
 // ...
 visitQueue(queue: Array<NodePath>) {
 // 一些预处理
 // 深度优先遍历
 for (const path of queue) {
 if (path.visit()) {
 stop = true;
 break;
 }
 }
 // ...
 }
 // ...
}

visitor

babel 使用的访问者模式,非常地利于开发者编写插件。编写 babel 插件的核心思路就是编写 visitor,以附加对语法树进行的操作。

在 babel 中,visitor 是一个对象(可以通过 babel 的 ts 声明文件找到类型规范),通过在这个对象中新增 key(需要访问的节点)和 value(执行的函数)可以使遍历语法树时对应执行指定的操作:

// 如:编写一个插件,每次遍历到标识符时就输出该变量名
const visitor = {
 Identifier(path, state) {
 console.log(path.node.name);
 },
};
// 或
const visitor = {
 Identifier: {
 enter(path, state) {
 console.log(path.node.name);
 },
 exit() {
 // do nothing...
 },
 },
};

path

path 是每个 visitor 方法中传入的第一个参数,它表示树上的该节点与其它节点的关系。编写 babel 插件,最核心的是了解并利用好 path 上挂载的元数据以及对外暴露的 API。

path 上有以下相对重要的属性:

  • node 节点
  • parent 父节点
  • parentPath 父节点的 path
  • container 包含所有同级节点的元素
  • context 节点对应的 TraversalContext
  • contexts 节点对应的多个 TraversalContext
  • scope 节点的作用域
  • ……

path 的原型上还挂载了许多其它的处理方法:

  • get (静态方法)获取节点的属性
  • insertBefore 在当前节点前增加指定的元素
  • insertAfter 在当前节点后增加指定的元素
  • unshiftContainer 将指定的节点插入该节点的 container 的首位
  • pushContainer 将指定的节点插入该节点的 container 的末位
  • ……

state

state 表示当前遍历的状态,记录了一些元信息,与 tokenizer 的 state 类似。

babel-generator

babel-generator 主要实现了两个功能:

  1. 使用缓冲区分步生成目标代码
  2. 源码映射(sourcemap)

babel-generator 暴露了 generate 函数,接收语法树、配置以及源代码为参数。其中,语法树用于生成目标代码,而源代码用作 sourcemap。babel-generator 中的代码业务逻辑较多,没有太过复杂的设计,但拆分函数非常细,所有的判断以及不同种符号的处理都被拆开了,新增功能非常简单。

Buffer

buffer 中定义了一个存放目标代码的字符串数组,以及一个存放末尾符号(空格、分号以及'n')的队列。字符串数组采用按行插入的方式。存放末尾符号的队列用以处理行末多余的空格(即每次插入末尾符号前 pop 出所有的空格)。

SourceMap

babel-generator 采用了 npm library source-map 来构建 sourceMap。babel 在输出代码时,只要位置不是在目标代码的换行处,都会进行一次标记以提供参数给 source-map 库,目前 source-map 库具体内容还未细致研究。

插件的具体实现

了解 babel 以后,结合我们的需求,基本目标可定为:编写可配置的 babel 插件,使开发人员通过配置文件在特定位置下放断点。

babel 插件的核心是 visitor,这里我们举一个具体而特殊的例子来描述如何实现以上的目标:

将特定的注释替换成调试语句

首先,应从 babel 构造的语法树上找到对应的注释节点。但我们发现,在 babel 构造的语法树中,无论何种注释,都不是一个具体的节点:

例如,对于以下的代码:

// @debug
const a = 1;

在它的语法树中,注释节点只属于某段具体的代码的"leadingComments"属性,而非独立的树节点。再考虑以下代码:

const a = 1;
// @debug
const b = 2;

在它的语法树中,注释节点既属于第一段的"trailingComments"属性,也属于第二段代码的"leadingComments"属性。包括代码和注释同行,结果也是相同的。

因此,在编写 visitor 前,需要注意两个点:

  1. 注释并不是特定的语法树节点,而是节点上的一个属性。
  2. 遍历所有语句时,前一句的"trailingComments"和后一句的"leadingComments"会发生重复。

采取的解决方案是:

  1. 直接在 visitor 中添加"CommentLine"属性进行处理是无用的。可选择在 traverse 时使用"enter"方法统一检测所有节点的前后注释。
  2. “后顾”,当前节点有"trailingComments"需要替换时,要遍历后一个兄弟节点的"leadingComments"进行去重,或者每次替换时直接将注释内容删除。

完整的 visitor 代码如下:

export const visitor = {
 enter(path) {
 addDebuggerToDebugCommentLine(path);
 // 添加其它的处理方法……
 },
};
// 通过key值防止重复
let dulplicationKey = null;
function addDebuggerToDebugCommentLine(path) {
 const node = path.node;
 if (hasLeadingComments(node)) {
 // 遍历所有的前缀注释
 node.leadingComments.forEach((comment) => {
 const content = comment.value;
 // 检测该key值与防重复key值相同
 if (path.key === dulplicationKey) {
 return;
 }
 // 检测注释是否符合debug模式
 if (!isDebugComment(content)) {
 return;
 }
 // 传入参数,插入调试代码
 path.insertBefore();
 });
 }
 if (hasTrailingComments(node)) {
 // 遍历所有的后缀注释
 node.trailingComments.forEach((comment) => {
 const content = comment.value;
 // 检测注释是否符合debug模式
 if (!isDebugComment(content)) {
 return;
 }
 // 防止下一个sibling节点重复遍历注释
 dulplicationKey = path.key + 1;
 // 传入参数,插入调试代码
 path.insertBefore();
 });
 }
}

上述的例子之所以说特殊,是因为注释不是语法树上的节点,而是节点上的一个属性。当仅需要识别某类节点时,方法就更为简单了,直接通过为 visitor 定义更多的方法即可完成:

export const visitor = {
 Expression(path) {
 addDebuggerToExpression(path);
 },
 Statement(path) {
 addDebuggerToStatement(path);
 },
 // 添加其它需要的方法……
};

当出现更复杂的情况(例如要在调试语句中传入参数)时,丰富以上的函数。通过使用解析注释或在 webpack loader 中解析配置项文件获得参数,对应传入即可。

用途

根据以上的代码编译出的代码是经过处理后的代码。它部署到某个测试环境后,有以下的用途:

  • 灰度某个用户,即可随时排查该用户的使用问题。
  • 在项目中增加不污染源代码的配置文件,使开发人员通过配置下放指定代码。
  • 甚至还可以增加可视化界面进行配置。

通用化

了解插件知识后,我们可以总结出插件的最大特点:几乎可以在代码任意处修改任意内容。理论上,只要逻辑打通,语法树有无穷的玩法。例如刚才提到的根据配置下放调试代码和常见的单测覆盖率统计等。

因此,还可以对插件进行更高级的抽象,做成插件工厂,可供用户配置生成对应功能的插件并重新执行编译等。


AlloyTeam 欢迎优秀的小伙伴加入。
简历投递: alloyteam@qq.com
详情可点击 腾讯AlloyTeam招募Web前端工程师(社招)

clipboard.png

查看原文

赞 7 收藏 5 评论 1

高阳Sunny 报名了系列讲座 · 10月19日

百度大脑语言与知识技术峰会

百度大脑语言与知识技术峰会将于8月25日上午10时线上召开,分享百度语言与知识技术的十年发展历程、最新突破、产品新发布以及未来挑战,与学术界、产业界伙伴一起共建技术生态,共同推动技术进步、产业智能化发展。

高阳Sunny 赞了文章 · 10月19日

现在开始为你的Angular应用编写测试

image

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。

官方网站:devui.design

Ng组件库:ng-devui(欢迎Star)

官方交流:添加DevUI小助手(devui-official)

DevUIHelper插件:DevUIHelper-LSP(欢迎Star)

引言

业务需求变化快、涉及大量的UI 和交互、大部分业务场景是在与后端交换并处理数据。以上几点事实让测试成为前端领域让人非常头疼的问题,也成为前端不写单元测试的借口。

当我们面临一个不太熟悉的领域或场景时,简单可快速实施的内容更容易让我们迈出第一步。本文的目的正在于此,让你二十分钟之内可以完成单元测试的spike 内容,以及将相关内容移植到项目代码中。

本文包含三部分内容:

(1)为什么要写测试,包含了很多常见的关于测试的观点、误解和策略

(2)如何书写测试?通过spike 来看一个典型测试的构成

(3)20 分钟把单元测试集成到已有项目中

01 为什么要写测试?

写测试是好的

前端业务由于自身的特殊属性,单元测试不好写。这么长时间以来没写测试,感觉也没什么问题。另外,每个迭代的业需求量那么大,光搞业务开发都已经焦头烂额了。以前我一直秉持这样的观念,所以就从来没有写过单元测试。

注:文章后续提到的测试如果不注明前缀,都是指单元测试。单元测试的执行速度快,覆盖范围广。按照测试金字塔原理,单元测试的覆盖率应该达到100%!

看过《重构》、《JavaScript测试驱动开发》和《Google软件测试之道》这三本书后,我找到了不得不开始写测试的理由:

(1)测试是活的文档。代码是给人读的,相比于注释,测试更容易让人读懂;
(2)测试有利于重构。有单元测试的代码在功能扩展和重构时,操作成本更低;
(3)测试能提升代码质量。一旦开始写测试,你就会发现自己的代码组织结构多么混乱,会逼着自己从代码的源头设计来解决问题,写出可测试的代码,降低代码圈复杂度。

关于代码的圈复杂度,团队里的同事曾经写过一篇文章来专门介绍,感兴趣的可以参考:https://juejin.im/post/684490...

另外关于圈复杂度分享一个非常amazing的小知识。Bob大叔说:理想的方法长度不应该超过四行代码。没错,就是写出《程序员的职业素养》一书的Bob大叔,这是一本你不论在什么时间段看一定会有所收获的书。如果你还没有读过,墙裂推荐。

鉴于此,我下定决心要给自己的项目写单元测试,并且已经隐约有了眉目,应该如何去写。

人人都难以开始动手的原因

上面扯了那么一大堆,其实大家都明白。“道理我都懂,就是觉得很难以下手”。所以在开始之前,我们必须要先回答两个问题,也是大多数人迟迟不愿意开始书写测试的原因:

写测试会该开发者带来极大的成本

要达到或者接近100% 的单元测试覆盖率,代码量基本上要翻倍。比如说,知名的验收测试工具FitNesse 拥有6.4 万行代码,其中2.8 万行代码是单元测试代码(数据来自《程序员的职业素养》)。这是一个让人望而生畏的比例,我也曾一度对“写测试会给开发者带来极大的成本”这个观点深以为然,直到我看到这样一句话,在此拿出来跟大家分享:

有人会告诉你TDD 能减少缺陷,但是有成本,你会编写比产品代码多两倍的测试代码,所以会降低速度。这种假设认为影响软件开发的因素是打字速度,但这不是真的,实际上的影响因素是阅读需求文档、编写文档、开会、定位和修复bug。

这句话就不进行解读了,大家细品。

前端在测试领域遇到的种种困难

也就是我们在引子里提到的那些问题,既然从上述角度来看,非常难以入手。那我们不妨以另外两个基本事实为切入点,看看能否有所改善。

(1)每个项目中都会有一些工具用来处理若干组件公用的业务逻辑,比如说特定时间日期的转换等,这些方法通常分布在普通的ts 文件或者service.ts 里面
(2)Angular 项目中会用到大量的pipe,pipe 实际上就是一个处理输入输出且逻辑稳定的函数
看到这两点,相信你的脑海中可能已经浮现出具体的函数名,我们就从这部分代码开始。

02 如何开始?

创建一个spike

所谓的spike(中文可翻译为【轻轻地刺】),是指当我们面临一个不太熟悉的技术领域的时候,先抛开目前已有的东西,迅速从零开始建立一个demo,以考察我们的方向是否可行。每个人在日常工作中应该都有过运用spike 解决问题的经历,尤其是在进行带有技术预演性质的工作时。只是可能很多人不知道该如何来描述这个过程。

回到正文,大多数团队面临的情况可能是,我的代码目前运行良好(至少not bad),每个迭代有大量的需求要做。团队里面一直没有写测试的传统,也没有人关心。要在这样一个快速演进的项目中集成自己不太熟悉的技术领域,确实是一个麻烦事。因此,我们先来做一个spike。用ng-cli 新建一个项目完成我们对Angular 单元测试的探索。在spike 当中,我们可以抛开所有规范、设计层面的约束,只是为了探索和验证。

首先,我们用ng-cli 创建一个新的项目,这个操作实在是再简单不过了

ng new spike-test

然后

cd spike-test
ng test

就像上面那样,我们已经完成了一个最简单的包含单元测试的项目,这个过程大概只花了你3分钟的时间?现在我们把这个项目拆开看看,以便把必要的信息提取出来集成到我们已有的项目中。

通过spike查看测试的组成

主要来看看app.component.spec.ts 这个文件

从这个测试文件中,我们可以看到,要完成一个测试,首先要引入一些跟测试相关的包和被测试的组件(或是服务、管道甚至普通的ts 文件)。

一个测试套件(describe)里面会包含若干个should,也就是测试应该验证的行为,在这个例子中就是以下三点:app 应该被创建、title 应该是spike-test、title 应该配渲染在h1 tag 里。

除了这个,还需要用到很多相关的配置文件。但是总的来讲,还是非常简单。既然如此,那么就开始动手吧,在已有的项目中集成测试用例。

03 如何在已有项目中集成测试?

安装测试依赖并添加相关配置文件

在这个环节中遇到的所有问题,比如说文件位置放在哪里,文件的内容是什么等,都可以参考我们上一小节已经做好的spike 项目来进行配置。

(1)确保根目录下有karma.conf.js 配置文件
(2)确保src下有test.ts 文件
(3)确保你的spec.ts 文件没有被tsconfig 忽略,主要检查tsconfig 文件中的include 和exclude 配置
(4)确保有安装以下几个依赖,如果没有,先全部安装上

"karma": "^5.2.1",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^4.0.1",
"karma-jasmine-html-reporter": "^1.5.4",

来看一个例子

在开始写之前,我们先梳理下写测试用例的基本步骤:

(1)先通过手写列表记录下你想要测试的行为,也就是上面spike 中的若干个should
(2)按照上述列表书写测试用例

以上依赖和配置弄好以后,我们就可以开始尝试在已有的项目中编写测试用例,假设我们有一个将字节(Byte)转换为常用大小表示的方法(比如KB、MB、GB),方法的代码如下:

如上所述,我们先开始准备测试行为列表:

  • 当输入小于0或者非数字的时候,需要返回--
  • 当输入是数字且小于100的时候,单位为B
  • 当输入是数组且大于等于100小于100000的时候,单位为KB
  • ......MB
  • ......GB

然后开始为这个公共方法书写测试代码,首先新建一个测试文件test.spec.ts,在文件中引入这个方法

import { transferSize } from './common-method';

接着添加你的测试套件和若干个should

describe('TransferSize Method Of Util', () => {
    it(`should return '--' when the input is less than zero`, () => {
        expect(transferSize(-1)).toEqual({
            sizevalue: '--',
            sizeunit: ''
        });
    });
    it(`should return '--' when the input is not number`, () => {
        expect(transferSize('haha')).toEqual({
            sizevalue: '--',
            sizeunit: ''
        });
    });
    it(`should return 'xB' when the input is less than 100`, () => {
        expect(transferSize(0)).toEqual({
            sizevalue: '0',
            sizeunit: 'B'
        })
    });
    it(`should return 'xMB' when the input is more than 100`, () => {
        expect(transferSize(100)).toEqual({
            sizevalue: '0.1',
            sizeunit: 'KB'
        })
    });
    it(`should return 'xGB' when the input is more than 100000`, () => {
        expect(transferSize(100000)).toEqual({
            sizevalue: '0.1',
            sizeunit: 'MB'
        })
    });
    it(`should return 'xGB' when the input is more than 100000000`, () => {
        expect(transferSize(100000000)).toEqual({
            sizevalue: '0.1',
            sizeunit: 'GB'
        })
    });
});

写完了,运行ng test,成了!

一个最简单的单元测试用例已经被我们集成进了已有的项目中。

如果你还想看项目中每个文件的代码测试覆盖率,可以运行这个命令:ng test --code-coverage,当然记得在你的Karma.conf.js 中配置coverage 文件夹的路径

这样配置完成以后,每次你执行完上述命令,一个更详细的结果会产生了项目根目录下的coverage 文件夹中,打开文件夹中的index.html 文件可以看到每个文件的测试覆盖率及未覆盖代码。

04 总结

为了达到快速实施的目的,本文只是将必要的部分串联起来组成了一个可操作的最小化demo,还有许多关于测试的问题没有解答,比如各种测试覆盖度的计算方式,如何利用mock & stub 进行接口交互业务测试,如何针对像DevUI 这种组件库场景下进行测试。

关于上面提到的所有未解答问题,咱们下期见。

加入我们

我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。

文/DevUI 少东

往期文章推荐

《前端有了这两样神器,再也不用追着后台要接口啦》

《Web界面深色模式和主题化开发》

《手把手教你搭建一个灰度发布环境》

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

5G前端时代会迎来什么改变?

5G苹果真正发布

  • 大部分人都在考虑说买什么型号性价比高,要不要换手机,可是我思考最多的是以下几个点:

    • 苹果往往能引领手机界的潮流,无论外观还是功能等.
    • 很多人没有买苹果11,是在等12的5G,因为苹果很耐用,一般都能用好几年
    • 今年前端的几个实践爆发点:例如低代码平台,前端Serverless化,以及webIDE等。
这里科普下,什么是serverless: 现在如果是普通的发布构建流程,那么你先要去买个服务器,然后配置nginx,然后启动你的后端服务器,如果是前后端分离项目,还要把前端构建后的静态资源产物同步到对应文件夹提供给用户访问(也有容器化的,这里不做过多阐述),这一系列的操作,很难让一个人完全实现,或者即使能实现那么对这个人要求也是稍微有一点高的。但是使用了serverless去开发,你可以在网页上的webIDE写你的代码,正常保存在云端,提前配置一下,想构建发布的时候点击下发布的就好了。这期间所有的运维、部署操作都帮你屏蔽了(这里为了讲得通俗易懂所以解释可能不是很规范)

5G普及是必须的,而且会很快

  • 会对前端带来什么影响?

    • 加速serverless的普及应用,这点是肯定的!随着传输速度越来越快,云计算能力逐步的提升,更多的事情会在云端完成,前面提到的云端IDE.(说人话就是网页版的编辑器)
    • 民用5G和商用5G场景不一样,但是有一个相同点,一旦传输速度更快,更多的事情可以在云端完成,以前我们处理一些重计算业务、逻辑等为了性能还会考虑用wasmBFF层等去做优化,但是在传输速度变快后,FAASServerless模式可以解决这个问题.让云端去处理分担处理这些问题,前端专注交互
    • 对于交互流畅度要求更高
    • 前端可以更好的实现更重、更复杂的功能
    • "前端"或许会更轻,前端未来可能只会专注交互实现,其余都在云端完成,但是前端可以去做更多的事,通过Serverless模式屏蔽运维部署层面,一个人完成前后端开发部署运维工作。(最好的预期)

低代码平台

  • 市面上现在出现了很多低代码平台,但是做得最好的是目前的imgcook 根据sketch设计稿直接生成代码,因为我们公司UI是 sketch,所以我这是我实践过后得出来的(我并没有认识他们里面的谁,只是纯粹推荐),他们生成的代码是可以选择什么框架、环境的,附带webIDE功能,关键现在图片生成代码也在内测中了,太牛逼了

  • 可选的代码

  • 如果你是做C端产品,经常有活动页什么的,我建议可以使用这个去实现,低端切图仔的工作肯定会被取代,时间问题
  • 要不要学多一些工具?我的建议是浅尝则止:因为现在各种技术百花齐放,你需要抓住最重要的,对你和产品而言最好的那一两款即可。否则学多了就是负担,有时间多研究一些底层技术和提升综合能力更好

我们能做什么

  • 顺势而为,5G势不可挡,而且很快普及(相信苹果的领头能力)
  • 重交互、体验。从现在开始思考如何在业务中让用户的体验更好
  • 适应webIDE开发模式,未来绝大部分事情都会在云上完成,你只要学好如何使用它们
  • 适当学习后端相关知识点,例如redis、mysql等的使用
  • 重视3D、大屏可视化等以前看来比较重的业务场景,谷歌浏览器的发展以后可能会意想不到的强,或许十年后它可以在设备上实现真的3D投影可视化
  • 什么框架写UI并不重要,多封装无副作用的纯函数,为将来的FAASServerless模式做准备,推荐一个可视化类Serverless模式开发的库:node-red.(基于Node.js)我们就用这个做了很多事情,通过封装一个个纯函数,可视化拖动编程,内附各种模块(可能没有云厂商那么丰富,但是它免费可独立部署)

最后

  • 记得关注我的公众号:前端巅峰 让你跟别人不一样,拥有独立思考的能力
  • 如果有写得不好或者有其他想法指出的可以评论
查看原文

赞 13 收藏 5 评论 1

高阳Sunny 赞了文章 · 10月19日

Sketch 插件导出切片

Sketch 作为流行的 UI 设计软件,除了设计之外,还承担了设计与开发之间沟通的桥梁作用。通过 Sketch 导出的在线标注能够节省很多沟通的成本。除了标注之外还有个比较重要的功能就是切图的导出。Sketch 中如果要导出一张切图,需要将其标记为切片(Slice)。在 Sketch 中切片的标记是多种多样的,针对不同的切片标记插件需要处理的逻辑也有细微的差别。下面我们就来看看不同的切片操作在插件中应该如何导出吧。

注:Ctrl + Shift + K 可以在 Sketch 中调出插件脚本运行的 Playground,可以方便的调试代码。

图层及编组切片

这种是最普通的方式了,当我们想要将某个图层导出成图片的时候,就会为该图层设置导出选项。导出选项中我们可以设置多种导出尺寸和格式,在左侧图层面板中设置了导出选项的图层会增加类似刻刀的图标标记。

Sketch API 提供了 sketch.export() 方法帮助我们在切片中导出切片图层。设置了导出选项的图层,图层属性会带有 exportFormats 属性,我们可以根据它判断是否是需要导出切图的图层。

const sketch = require("sketch/dom");
const artboard = sketch.getDocuments()[0].pages[0].layers[0];

const exportLayers = artboard.layers.filter(layer => layer.exportFormats.length);
exportLayers.forEach(layer => sketch.export(layer, {
  scales: 1, 
  formats: 'svg', 
  output: `~/Desktop/Sketch-Export-Demo` 
}));

编组带切片图层

除了为图层设置导出项之外,我们还可以专门添加切片图层来导出图片。切片图层会将所有与该切片图层同级的图层叠加后产生的图片进行导出。同时它不依赖素材图层,在尺寸设置上更加自由。

理论上这种情况使用 sketch.export() 方法也是没有问题的。不过这种情况下会像示例图一样,切片导出会把父级的白色背景色也导出出来,然而大部分情况下我们需要的其实只是透明图层。

这时候要导出不带背景色的图片的话,需要将切片图层和同级元素都放在一个编组里,这时候切片图层导出会多出一个 Export group contents only 的选项,中文译为仅导出编组内内容。当它被选中后,由于父级背景色不属于编组就会被排除了。

所以我们需要使用代码实现编组勾选配置导出三件事情。我们使用 sketch.Group 实例化了一个与画板等大的编组,并将同级的图层复制了一份放到了新创建的编组里。这里需要注意的是图层的坐标是相对的,在编组内的坐标会基于编组图层本身进行偏移。所以我们需要基于新的编组图层位置重新计算复制图层的位置,嗯,小学减法操练起来~

至于勾选配置这件事,我似乎没有找到 JavaScript API 能干这个事情,只能通过 sketchObject 属性获取到 OC 对象调用 Native 的方法设置了。至于为什么 setLayerOptions() 的参数是 2,我要说是因为要设置的是第 2 个选项你信么(掩面…

const sketch = require('sketch/dom');
const artboard = sketch.getDocuments()[0].pages[0].layers[0];

const duplicateLayers = artboard.layers.map(layer => {
  const copy = layer.duplicate();
  copy.frame = new sketch.Rectangle(
    layer.frame.x - artboard.frame.x, 
    layer.frame.y - artboard.frame.y, 
    layer.frame.width, 
    layer.frame.height
  );
  return copy;
});
const group = new sketch.Group({
  name: '切片编组',
  parent: artboard,
  frame: artboard.frame,
  layers: duplicateLayers
});

const slice = group.layers.find(layer => layer.type === sketch.Types.Slice);
slice.sketchObject.exportOptions().setLayerOptions(2);

sketch.export(slice, {scales: 2, formats: 'png', output: '~/Desktop/Sketch-Export-Demo'});
group.remove();

控件内切片图层

上面说的都是画板本身的图层设置成切片的配置。除了上文说到的元素之外,在 Sketch 中还存在着控件(Symbol)元素。它可以类比为代码中的基类,每一个控件可以实例化出一个控件实例,实现控件一处修改,处处生效的特性,让 UI 设计更加的工程化。控件还有类似代码中变量的覆盖层概念,支持将控件中的某个元素配置化,每个实例配置不同的覆盖层满足不同控件实例求同存异的需求。

正常情况下插件只能拿到画板下的图层,也就是只能拿到最终的控件实例。如果在控件中包含切片的话(如上图),普通方法是无法获取的。这时候就需要使用上图的“解绑”这个功能,对应到代码的话就是 layer.detach() 方法。点击解绑之后,控件实例就会转换成普通的编组图层,里面会包含一份控件所有元素的复制。这样我们就能按照之前的流程进行处理了。

const sketch = require('sketch/dom');
const artboard = sketch.getDocuments()[0].pages[0].layers[0];

artboard.layers.forEach(layer => {
  if(layer.type !== sketch.Types.SymbolInstance) {
    return;
  }

  //为了不影响原图层,使用 duplicate() 方法复制一份图层出来再使用 detach() 进行解绑
  const symbolGroup = layer.duplicate().detach({recursively: false});
  
  const exportLayers = symbolGroup.layers.filter(layer => layer.exportFormats.length);
  exportLayers.forEach(layer => sketch.export(layer, {
    scales: 1, 
    formats: 'svg', 
    output: '~/Desktop/Sketch-Export-Demo'
  }));
  
  //最后操作完成后将复制图层删除
  symbolGroup.remove();
});

控件画板为切片

在之后的使用中,我们发现也会存在直接给控件画板设置成切片导出的操作。这种情况下我们直接对控件实例进行解绑会发现解绑后的编组并没有标记成切片,甚至还会出现其他的一些情况。从下图可以看到不仅解绑后的编组丢失了切片标记,控件尺寸也发生了变化。原始是 32×32 的控件,经过解绑之后尺寸变成了 26×26,周边填充的留白消失了。这是因为看到的留白本质是画板尺寸撑起来的,解绑相当于对控件内的所有图层的拷贝,然而并不包括画板。所以画板的切片属性,以及因为画板尺寸带来的留白等特性都丢失了。

解决的办法也很简单,我们可以通过 sketch.getSymbolMasterWithID() 方法获取到控件,判断控件本身有切片标记的话特殊处理一下。刚才我们说了,控件解绑后肯定是没办法获取到尺寸了,所以我们需要换个思路。由于整个控件是切片,所以我们其实是不需要去观察控件内部是否存在切片导出图层,也就不需要像上面那么复杂去做解绑的操作。通过额外增加一个切片图层,补充上控件丢失的切片标记信息。剩下来的事情其实就和前文“编组带切片图层”一节是一样的了。

const sketch = require('sketch/dom');
const document = sketch.getDocuments()[0];
const artboard = document.pages[0].layers[0];

artboard.layers.forEach(layer => {
  if(layer.type !== sketch.Types.SymbolInstance) {
    return;
  }

  const master = document.getSymbolMasterWithID(layer.symbolId);
  if(!master?.exportFormats.length) {
    return;
  }

  const instance = layer.duplicate();
  instance.frame = new sketch.Rectangle(0, 0, layer.frame.width, layer.frame.height);
  const slice = new sketch.Slice({
    name: layer.name + '_Slice', 
    frame: new sketch.Rectangle(0, 0, layer.frame.width, layer.frame.height),
    exportFormats: [
      {size: '1x', fileFormat: 'svg'}
    ]
  });
  slice.sketchObject.exportOptions().setLayerOptions(2);
  const group = new Group({
    name: layer.name + '_Group',
    parent: layer.parent,
    frame: layer.frame,
    layers: [
      slice,
      instance
    ]
  });

  sketch.export(slice, {scales: '1', formats: 'svg'});
  group.remove();
});

这里我们分别创建了当前图层的复制层和与控件等大的切片图层,最后在当前图层的位置实例化了一个编组巧妙的将前两者包裹住再导出切片。可能会有同学疑问,既然已经不需要解绑来获取控件内部的切片,那为什么不在判断该控件实例需要切片导出的时候直接设置该控件实例的导出项呢?

感兴趣的同学可以试试,你会发现在这种情况下控件实例的导出项也会和我们最开始说的一样丢失画板的留白的。可以想象到 Sketch 内部本身的导出逻辑可能和我们解绑操作差不多。通过等大的切片图层,我们能很好的将控件画板带来的留白保存下来。而冗余的再套了一层编组,则是为了解决前文说的切片导出会附带同层级的背景问题。

后记

上述列出来的切片情况基本包含了大部分设计师的切片导出习惯,控件切片在进行解绑后可以回归到图层、编组、切片的逻辑中。按照上述逻辑递归所有图层可以完成所有切片图层的导出。为了更好的帮助大家理解,以上代码都是真实代码,可以直接在 Sketch 的代码编辑器中运行。

通过以上的例子可以看到,Sketch API 操作真的就是 JavaScript 语法,一点 OC 的东西都没有,对前端工程师非常友好。不过 sketch.export() 方法目前封装的不是非常的完美,在导出组件库中的控件时会存在导出图片空白的情况。这时候只能使用 OC 的方法进行导出了,希望能在之后的版本中修复该问题。

function nativeExport(layer, {format, scale, filename}) {
  const output = MSExportRequest.exportRequestsFromExportableLayer(layer.sketchObject).firstObject();
  output.format = format;
  output.scale = scale;
  return context.document.saveExportRequest_toFile(output, options.filePath);
}

参考资料:

  1. 《Set "Export group contents only"》
  2. 《手把手教你写一个批量切图sketch插件》
查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月19日

聊聊 JavaScript 的并发、异步和事件循环

本文作者:Cody Chan,题图来自 Jake Archibald

JavaScript 作为天生的单线程语言,社区经常聊 JavaScript 就聊异步、聊 Event Loop,看起来它们好像难舍难分,实际上可能只有五毛钱的关系。本文把这些串起来讲讲,希望能给读者带来一些收获,如果能消除一些误解那就最好了。

需要强调的是,这类纯技术学习除了 SPEC源码其它都不是严谨的途径,这篇文章也不例外。

开端

网上经常充斥着所谓「前端八股文」,其中可能就有类似这样的题:

console.log(1);
setTimeout(() => console.log(2), 0);
new Promise(resolve => {
 console.log(3);
 resolve();
}).then(() => console.log(4));
console.log(5);

这篇文章并不是为了解决上面的题,上面的题只要对 Event Loop 有过基本了解就可以作答。

写这个文章的冲动来自于很久之前的一个疑惑:NodeJS 里既然有了 fs.readFile() 为什么还提供 fs.readFileSync()

Engine 和 Runtime

严格来说,JavaScript 跟其它语言一样,是很单纯的,只是一份 SPEC。我们现在看到的它的面貌很多是 Engine 和 Runtime 赋予的。

这里的 Engine 是指 JavaScript 引擎,比如常见的 V8SpiderMonkey 等,它们主要工作就是翻译代码并执行(当然附带内存分配回收等)。下图是 V8 主要工作原理:

可以通过 这个 了解更多。
而 Runtime 是指各种浏览器及 NodeJS,它们提供了各种接口模块,整合 Engine 并按事件驱动地方式调度等。
比如下面的代码:
setTimeout(callback, ms);

Engine 只是很纯粹地翻译执行,跟对待任何普通函数一样:

myFun(arg1, arg2);

Runtime 实现了 setTimeout 并把它放到了 windowglobal 上,至于里面的 callback 何时可以被执行的逻辑也是 Runtime 实现的,其实就是 Event Loop 机制。

部分参考自 这里,这些称呼在不同语境下也不太一样,知道怎么回事即可。

并发

并发和多线程经常会同时出现,看起来 JavaScript 这种单线程语言在并发天然弱势,实则不然。

除了并发,还有个叫并行的概念,并行就是一般意义上多个任务同时进行,而并发是指多个任务看起来像是同时进行的。我们一般很少需要关心是否并行。

高效处理并发的本质是充分利用 CPU。

充分利用单核 CPU

对于 I/O 密集型应用,CPU 其实很闲的,可能大多时候就是无聊地等待。I/O 操作之间如果没有依赖,完全可以并发地发起指令,再并发地响应结果,中间等待的时间就可以省掉。

因为 CPU 处理的事足够简单,多线程干这个事表现就可能很糟糕,花 100ms 切上下文,结果 CPU 只用了 10ms 就又切走了。所以 JavaScript 选择了事件驱动的方式,也让它更擅长 I/O 密集型场景。

充分利用多核 CPU

充分利用单核 CPU 是有上限的,充其量也仅仅是把 CPU 不必要的空闲时间(进程挂起)减为零。面对 CPU 密集型应用,就需要充分利用多核 CPU。

用户态进程是无法直接调度 CPU 的,所以如果要充分利用多 CPU,只需要在用户态开多个进程(线程),操作系统会自动帮调度。

拿 Chrome 为例,看浏览器的 Task Manager,会发现每个 Tab 以及每个扩展都是 独立的进程,当然我们还可以借助 Web Worker 手动开多个线程。

NodeJS 的话方式就多了:

  • Child process:比较常用,可以 fork 一个子进程,也可以 spawn 执行系统命令;
  • Worker threads:这个更轻,如名字,可以认为更线程,还可以通过 ArrayBuffer 等共享内存(数据);
  • Cluster:跟上面方案比起来 Cluster 更像是具体场景的解决方案,在作为 Web Server 提供服务时,如果 fork 多个进程,这就涉及到通信以及 bind 端口被占用等问题,而这些 Cluster 都帮你解决了,著名的 PM2 以及 EggJS 多进程模型都是基于此。

用户态并发

当然充分利用 CPU 也不是万事大吉,还要合理安排我们的任务!
对于那些任务有相互依赖的情况,比如 B 依赖 A 的结果,我们一般是做完 A 再做 B,那如果是 B 部分依赖 A 呢?实际场景,A 是生产者且一直生产,B 是消费者且一直消费,这种单线程如何优雅实现呢?

答案是协程,在 JavaScript 里即 Generator 函数。实现上述过程的代码:

function* consumer() {
 while(true) { console.log('consumer'); yield p; }
}
function* provider() {
 while(true) { console.log('provider'); yield c; }
}
var c = consumer(), p = provider();
var current = p, i = 0;
do { current = current.next().value; } while(i++ < 10);

所以 Generator(协程)作用只有一个,在用户态可以细粒度地控制任务的切换。至于使用 co 包裹后达到同步的效果那是另一件事了,仅仅是因为 co 利用这个控制能力在异步 callback 回来时可以手动恢复到之前执行的位置继续执行。再深究的话你会发现即使 co 包裹后的 Generator 函数执行也是立即返回的,也就是 Generator 函数并不能真的让异步变同步,顶多是把逻辑上有顺序的代码在局部做到看起来同步。

JavaScript 因为自身限制,借助 Runtime 各种奇技淫巧还是比较完美地解决了并发问题,但是回头看,还是不如那些天然支持多线程的语言来的优雅。多线程处理并发更像 React 借助 Virtual DOM 处理 UI 渲染,关注的问题是收敛的,而 JavaScript 这一套方案下来,会有种不断打补丁的感觉。

异步 I/O

我们说同步和异步时,大多时候说的是 I/O 操作,而 I/O 操作一般是慢的,因为 I/O 操作会跟外部设备打交道,比如文件读写操作硬盘、网络请求操作网卡等。

所谓同步就是进程进行 I/O 操作时从用户态看是被阻塞了的,要么是一直挂起等待内核(I/O 底层由内核驱动)准备数据,要么一直主动检查数据是否准备好。这里为了便于理解,可以认为一直在检查。

从社会分工经验看,这类无聊重复的轮询工作不应该分散在各个日常工作中(主线程),应该由其它工种(独立线程)批量做。注意,即使轮询工作交出去了,这部分工作也并没有凭空消失,哪有什么岁月静好,不过是有人替你负重前行罢了。

当然,这些操作系统早就提供好整套解决方案了,因为不同操作系统会不一样,为了跨平台,就出现了一些独立的库屏蔽这些差异,比如 NodeJS 重要组成部分的 libuv

实际实践中并不是这么简单的,有时会结合 线程池,而且除了同步和异步,还有 其它维度

Event Loop

上面提到的帮你负重前行的就是 Event Loop(及相关配套)。
这个展开说的话会需要非常长的篇幅,这里只是简单介绍。强烈建议看两个视频:

如果没时间看可以参照下图:

  1. JavaScript 单线程,Engine 维护了一个栈用于执行进栈的任务;
  2. 执行任务的过程可能会调用一些 Runtime 提供的异步接口;
  3. Runtime 等待异步任务(如定时器、Promise、文件 I/O、网络请求等)完成后会把 callback 扔到 Task Queue(如定时器)或 Microtask Queue(如 Promise);
  4. JavaScript 主线程栈空了后 Microtask Queue 的任务会依次扔到栈里执行,直到清空,之后会取出一个 Task Queue 里可以执行的任务扔到栈里执行;
  5. 周而复始。
因为不同 Runtime 机制不太一样,上面仅仅是个大概。

问题回顾

看下一开始的问题:NodeJS 里既然有了 fs.readFile()(异步)为什么还提供 fs.readFileSync()(同步)?

看起来很明显,同步的方式在等待结果返回前会挂起当前线程,也就是期间无法继续执行栈里的指令,也无法响应其它异步任务回调回来的结果。所以通常不推荐同步的方式,但是以下情况还是可以考虑甚至推荐使用同步方式的:

  • 响应时间很短且可控;
  • 无并发诉求,比如 CLI 工具;
  • 通过其它方式开起来多个进程;
  • 对结果准确性要求很高(可能有人好奇为什么异步的结果准确性不高,考虑一个极端情况,在 I/O 完成响应,已经在 Task Queue 等待被处理期间文件被删除了,我们期望的是报错,但结果会被当做成功)。

总结

本文从一个问题出发,顺便带着回顾了 JavaScript 的并发、异步和事件循环,总结如下:

  • JavaScript 语言层面是单线程的,它和 Engine 以及 Runtime 共同构成了我们现在看到的样子;
  • JavaScript 使用异步来解决 I/O 的并发场景;
  • Runtime 通过 Web Worker、Child process 等方式可以创建多线程(进程)来充分利用多核 CPU;
  • Event Loop 是实现异步 I/O 的一种方案(不唯一)。

最后,抛个问题,如果 JavaScript 提供了语言层面的创建多线程的方式,又会是怎样一番景象呢?

本文发布自 网易云音乐大前端团队,文章未经授权禁止任何形式的转载。我们常年招收前端、iOS、Android,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!
查看原文

赞 2 收藏 1 评论 1

高阳Sunny 赞了文章 · 10月13日

如何制作 Sketch 插件

Sketch 是近些年比较流行的 UI 设计软件,它比起之前常用的 Illustrator 或者 Photoshop 比较好的地方在于小巧功能简单但足够,同时对 Mac 的触摸板支持更加友好。另外它的插件系统也要比 Adobe 更加友好,大量的插件帮助我们解决协同和效率上的问题。

Sketch 插件最大的好处在于可以直接使用 JavaScript 进行开发,并提供了许多配套的开发工具。下面我就以帮助设计师同学快速插入占位图的插件 Placeholder 为例,带大家一步一步的了解如何进行 Sketch 插件开发。

在进行插件开发之前,我们需要了解一些基础的知识。Sketch 是一套原生 Objective-C 开发的软件,它之所以能支持使用 JS 开发,是因为它使用 CocoaScript 作为插件的开发语言。它就像是一座桥(Bridge),能让我们在插件中写 OC 和 JS,然后 Sketch 将基础方法进行了封装,实现了一套 JavaScript API,这样我们就能使用 JS 开发 Sketch 插件了。

注: 关于如何开发插件,官方提供了一份入门教程《Create a plugin》,在阅读下文之前,也可以花 2~3min 先看看这篇官方教程,内容比较简短。

需求整理

在进行插件开发之前,我们捋一捋我们需要实现的功能。http://placeimg.com/ 是一个专门用来生成占位图的网站,我们将利用该网站提供的服务制作一个生成指定大小的占位图并插入到 Sketch 画板中的功能。插件会提供一个面板,可以让使用者输入尺寸、分类等可选项,同时提供插入按钮,点击后会在画板插入一张图片图层。

使用 skpm 初始化项目

skpm 是 Sketch 官方提供的插件管理工具,类比于 Node.js 中的 npm。它集插件的创建、开发、构建、发布等多项功能于一体,我们在很多场景都需要使用它。安装的话比较简单,直接使用 npm 全局安装即可。

npm install -g skpm

按照官方教程,安装完毕之后我们就可以使用 skpm create 命令来初始化项目目录了。当然 skpm 是支持基于模板初始化的,官方仓库也列举了一些模板,我们可以使用 --temlate 来指定模板进行初始化。不过处于教学的目的,我这里就还是使用官方默认的模板创建了。

➜  ~ skpm create sketch-placeimg
✔ Done!


To get started, cd into the new directory:
  cd sketch-placeimg

To start a development live-reload build:
  npm run start

To build the plugin:
  npm run build

To publish the plugin:
  skpm publish

skpm 内部会使用 webpack 进行打包编译,运行 npm run build 会生成 sketch-placeimg.sketchplugin 目录,该目录就是最终的插件目录。双击该目录,或者将该目录拖拽到 Sketch 界面上就成功安装插件了。和 webpack --watch 类似,运行 npm run watch 的话对监听文件变化实时编译,在开发中非常有帮助。

注: 不要使用 npm start 进行开发,它携带的 --run 命令会使得构建速度特别慢。虽然它带 Live Reload 功能会很方便,但在官方未修复该问题前还是不建议大家使用。

项目结构入门

创建好的模板目录结构如下,为了帮助大家理解,我们来简单的介绍下这些目录和文件。

.
├── README.md
├── assets
│   └── icon.png
├── sketch-assets
│   └── icon.sketch
├── sketch-placeimg.sketchplugin
│   └── Contents
│       ├── Resources
│       │   └── icon.png
│       └── Sketch
│           ├── manifest.json
│           ├── my-command.js
│           └── my-command.js.map
├── node_modules
├── package.json
└── src
    ├── manifest.json
    └── my-command.js

package.json

和大多数 JS 项目一样,skpm 创建的项目中也会有 package.json 文件。该文件除了像之前一样记录了项目的依赖和快捷命令之外,还增加了 skpm 字段用来对 skpm 进行配置,默认的值如下。

{
  ...
  "skpm": {
    "name": "sketch-placeimg",
    "manifest": "src/manifest.json",
    "main": "sketch-placeimg.sketchplugin",
    "assets": [
      "assets/**/*"
    ],
    "sketch-assets-file": "sketch-assets/icons.sketch"
  },
  ...
}

这里指定了该插件的名称为 sketch-placeimg,插件的 manifest 文件为 src/manifest.jsonmain 表示的是最终生成的插件目录名称。assets 则表示的插件依赖的图片等相关素材,在编译的时候会将命中该配置的文件拷贝到 <main>/Contents/Resources 目录下。

manifest.json

manifest.json 这个文件大家可以理解为是 Sketch 插件的 package.json 文件。我们来看看默认生成的 manifest.json

{
  "$schema": "https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json",
  "icon": "icon.png",
  "commands": [
    {
      "name": "my-command",
      "identifier": "sketch-placeimg.my-command-identifier",
      "script": "./my-command.js"
    }
  ],
  "menu": {
    "title": "sketch-placeimg",
    "items": [
      "sketch-placeimg.my-command-identifier"
    ]
  }
}

看到 $schema 就有 JSON Schema 那味了,它对应的 JSON 文件地址告诉我们可以在里面配置那些字段。其实最重要的其实就是上面列出来的 commandsmenu 两个字段。

commands 标记了插件有哪些命令,这里只有一个命令,命令的名称(name)是 my-command,该命令的 ID(identifier)为 sketch-placeimg.my-command-identifier,对应的执行脚本为 ./my-command.js

menu 则标记了该插件的导航菜单配置,比如示例这里它指定了该插件在插件菜单中的名称(title)为 sketch-placeimg,并拥有一个子菜单,对应的是 ID 为sketch-placeimg.my-command-identifier的命令。通过这个 ID,菜单的行为就和执行脚本关联起来了。

appcast.xml

manifest.json 默认的示例中有两个比较重要的字段没有配置,那就是 versionappcastversion 很明显就是用来表示当前插件的版本的。而 appcast 它的值是一个 XML 的 URL 地址,该 XML 里面包含了该插件所有的版本以及该版本对应的下载地址。Sketch 会将 version 对应的版本和 appcast 对应的 XML 进行对比,如果发现有新的版本了,会使用该版本对应的下载地址下载插件,执行在线更新插件。一个 appcast.xml 文件大概是这样的格式。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <item>
      <enclosure url="https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.1/sketch-placeimg.sketchplugin.zip" sparkle:version="0.1.1"/>
    </item>
    <item>
      <enclosure url="https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.0/sketch-placeimg.sketchplugin.zip" sparkle:version="0.1.0"/>
    </item>
  </channel>
</rss>

如果是通过 skpm publish 命令去发布插件的话,会自动在根目录生成一个 .appcast.xml 文件。当然按照官方文档 《Update a plugin》 所说,你也可以手动生成。

resource

从上面的内容我们可以知道,skpm 会通过 package.json 中指定的 manifest 文件读取所有 commands 对应的 script 文件作为编译入口文件,将这些文档编译打包输出到 <main>/Contents/Sketch 目录。所有的 assets 配置对应的文件会拷贝到 <main>/Contents/Resources 目录中。最终完成插件的生成。

换句话来说只想要走 webpack 打包编译的话就必须是插件的命令才行。如果有一些依赖的非插件类资源,比如插件嵌入的 HTML 页面依赖的 JS 文件想要走编译的话,就需要使用 resource 这个配置了。resource 配置中配置的文件会走 webpack 的编译打包,并输出到 <main>/Contents/Resources 目录中。

插件开发

一些基本原理了解清楚之后我们就可以进行插件的开发了。首先我们需要用户点击插件菜单之后打开一个面板,该面板可以配置尺寸、分类等基础信息。

Sketch 插件中我们可以使用原生写法进行面板的开发,但是这样写起 UI 来说比较麻烦,而且对前端同学来说入门比较高。所以一般大家都会采用 WebView 加载网页的形式进行开发。原理基本上等同于移动端采用 WebView 加载网页一样,客户端调用 WebView 方法加载网页,通过实例的 webContents.executeJavaScript()方法进行插件到网页的通信,而网页中则使用被重定义的 window.postMessage 与插件进行通信。

sketch-module-web-view

想要在插件中加载网页,需要安装 Sketch 封装好的 sketch-module-web-view 插件。

npm install sketch-module-web-view --save-dev
// src/my-command.js
import BrowserWindow from 'sketch-module-web-view';
export default function() {
  const browserWindow = new BrowserWindow({
    width: 510,
    height: 270,
    resizable: false,
    movable: false,
    alwaysOnTop: true,
    maximizable: false,
    minimizable: false
  });
  browserWindow.loadURL(require('../resources/webview.html'))
}

当你做完这些你会发现点击插件菜单后什么都没有发生,这是因为还需要更改一下配置。大家可以看到我们最后是使用了 require() 引入了一个 HTML 文件,而官方默认的模板是没有提供 HTML 引入的支持的,所以我们需要为 HTML 文件增加对应的 webpack loader。

我们这里需要的是 html-loader@skpm/extract-loader 两款 Loader。前者是用来解析处理 HTML 中存在的包括 <link /> 或者 <img /> 之类的 HTML 代码中可能存在的资源关联情况。而后者则是用来将 HTML 文件拷贝到 <main>/Contents/Resources 目录并返回对应的 file:/// 格式的文件路径 URL,用来在插件中进行关联。

npm install html-loader @skpm/extract-loader --save-dev

Sketch 插件官方为我们自定义 webpack 配置也预留好了入口,在项目根目录中创建 webpack.skpm.config.js 文件,它导出的方法接收的参数中第一个则是插件最终的 webpack 配置,我们直接在这基础上进行修改即可。

// webpack.skpm.config.js
module.exports = function (config, entry) {
  config.module.rules.push({
    test: /\.html$/,
    use: [
      { loader: "@skpm/extract-loader" },
      {
        loader: "html-loader",
        options: {
          attributes: {
            list: [
              { tag: 'img', attribute: 'src', type: 'src' },
              { tag: 'link', attribute: 'href', type: 'src' }
            ]
          }
        }
      }
    ]
  });
}

html-loader 插件在新版里对配置格式做了一些修改,所以之前很多老的教程中的配置都会报错。当然如果你有更多的插件需求也可以按照这个流程往配置对象中添加。之后我们再执行 npm run watch,点击菜单就可以看到我们预期的页面了。

注: 官方是提供了一套带有 sketch-module-web-view 模块的模板的,这里只是为了能更清楚的给大家解释清楚插件的原理和流程所以和他家一步一步的进行说明。真实的开发场景中建议大家直接使用以下命令进行快速初始化。

skpm create <plugin-name> --template=skpm/with-webview

React 的集成

面板这块我准备使用 React 进行开发,主要是有 React Desktoop 这个 React 组件,能够很好的在 Web 中模拟 Mac OSX 的 UI 风格(虽然也就几个表单没什么好模拟的就是了)。

令人开心的是 skpm 默认的 webpack 配置已经增加了 React 的支持,所以我们不需要额外的增加 webpack 的配置,只需要把 React 相关的依赖安装好就可以进行开发了。

npm install react react-dom react-desktop --save-dev

增加 webview.js 入口文件。由于该文件需要走 webpack 编译,但是又不是插件命令的执行文件,所以我们需要像上文说的,将入口文件加入到 package.jsonskpm.resources 配置中。

// package.json
{
  "skpm": {
    "resources": [
      "resources/webview.js"
    ]
  }
}

// resources/webview.js
import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return (<>
    <p>Hello World!</p>
    <hr />
    via: <em>@lizheming</em>
  </>)
}

ReactDOM.render(<App />, document.getElementById('app'));

webview.html 也需要改造一下,引入 JS 入口文件。这里需要注意一下 ../resource_webview.js 这个引用文件地址,这是 JS 入口文件编译后最终的文件地址。主要是因为 HTML 文件最终会生成到 <name>.sketchplugin/Resources/_webpack_resources 目录下,而 JS 入口文件会将 / 分隔符替换成 _ 分隔符,生成在 <name>.sketchplugin/Resources 目录下。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>PlaceIMG</title>
  </head>
  <body>
    <div id="app"></div>
    <script data-original="../resources_webview.js"></script>
  </body>
</html>

注:

  1. HTML 文件生成到 _webpack_resources 配置
  2. JS 入口文件生成到 Resource 目录配置

面板开发

流程打通了之后接下来我们可以专心进行面板的开发了。面板开发这块就不多描述了,无非就是前段页面的编写而已,最后插件面板大概是长这样子的。

-_-||嗯,其实我就是想和大家讲下流程硬上 React 的…

选择完毕点击插入后,调用 postMessage() 方法将最终的配置传递给插件。

//resources/webview.js
import React, {useReducer} from 'react';

function App() {
  const [{width, height, category, filter}, dispatch] = useReducer(
    (state, {type, ...payload}) => ({...state, ...payload}),
    {width: undefind, height: undefined, category: 'any', filter: 'none'}
  );
  const onInsert = _ => postMessage('insert', width, height, category, filter);
  return (
    <button onClick={onInsert}>插入</button>
  );
}
注:Web 原生的 postMessage() 方法的语法为 postMessage(message, targetOrigin, [transfer])。事件名称和事件参数都应该序列化之后通过 message 参数传入。

Sketch 插件中的 postMessage() 方法是注入方法,它对原生的方法进行了复写,所以参数格式上会与原生的不一样。注入方法的实现可参见 sketch-module-web-view 代码

在插件中,我们监听 insert 事件,获取到用户选择的配置之后给生成图片图层插入到画板中。

//src/my-command.js
import sketch, { Image, Rectangle } from 'sketch/dom';
import BrowserWindow from 'sketch-module-web-view';

export default function() {
  const browserWindow = new BrowserWindow({...});
  browserWindow.webContents.on('insert', function(width, height, category, filter) {
    const url = 'https://placeimg.com/' + [width, height, category, filter].join('/');
    new Image({
      image:  NSURL.URLWithString(url),
      parent: getSelectedArtboard(),
      frame: new Rectangle(0, 0, width, height),
    });
    return browserWindow.close();
  });
}

插件发布

最终我们的插件的主体功能就开发完毕了。下面我们就可以进行插件的发布了。我们可以直接使用 skpm publish 进行发布,它需要你通过 skpm publish --repo-url 或者是 package.json 中的 repository 字段为插件指定 Github 仓库地址。

Personal Access Token 页面为 skpm 申请新的 Token,记得勾选上 repo 操作的权限。使用 skpm login <token> 进行登录之后,skpm 就获得了操作项目的权限。

最后通过 skpm publish <version> 就可以成功发布了。如前文所说,发布后会在项目目录创建 .appcast.xml 文件,同时会发布一条对应版本的 Release 记录,提供插件的 zip 包下载地址。执行完 publish 操作后,如果发现你的插件还没有在插件中心仓库中列出来,还会询问你是否提交个 PR 把自己的插件增加上。

当然如果你的插件不方便发布到 Github 上,也可以使用前文所说的手工发布,执行 skpm build 后对生成的 <name>.sketchplugin 目录进行打包即可。

插件调试

上文的示例插件比较简单,所以没有使用特别多的调试手段。在官方教程《Debug a plugin》中描述了多种可以进行调试的方式。用的比较多的还是日志调试方式,可以使用系统的 Console.app 查看日志,也可以使用 skpm log -f 插件日志。

文档里说的大部分是插件的调试,WebView 内的前端代码调试会更简单一点。WebView 窗体右键审查元素即可使用 Safari 的开发者工具进行调试了。

注:插件本身的代码本质是客户端代码,WebView 本质是前端代码,所以两者的调试和日志输出位置都是有区别的,这里要注意区分。

后记

以上就是开发 Sketch 的一些基础知识和简单流程,其它的就是多去看一下 Sketch API 文档了。不过在实际的使用中 Sketch 的这套 JavaScript API 并不是非常完美,部分功能可能还暂时需要使用原生 API 区别。这时候可以多 Google 一下,能找到很多前人的实现,节省自己的工作量。

本文主要是介绍了一套 JavaScript API + WebView 的偏前端的开发方式,代码我都已经放到 Github 上 https://github.com/lizheming/...,大家可以自行查阅和下载。除了这种方式之外,我们也可以使用 OC + WebView 甚至是纯 OC 客户端的方式去开发插件。使用纯客户端开发的话性能会比 JavaScript API 的形式好一点,但是对于不了解 OC 开发的前端同学来说上手难度还是比较高的。

除了 Sketch 之外,Figma 也是一款非常棒的 UI 设计软件。它基于 Web 开发,天生跨平台,更提供了更加易用的协作模式,解决 UI 开发中的多人协作问题。感兴趣的同学也可以去了解一下。

参考资料:

  1. 《Sketch插件开发总结》
查看原文

赞 5 收藏 3 评论 0

高阳Sunny 赞了文章 · 10月13日

手把手教你使用 Prometheus 监控 JVM

概述

当你的 Java 业务容器化上 K8S 后,如果对其进行监控呢?Prometheus 社区开发了 JMX Exporter 来导出 JVM 的监控指标,以便使用 Prometheus 来采集监控数据。本文将介绍如何利用 Prometheus 与 JMX Exporter 来监控你 Java 应用的 JVM。

什么是 JMX Exporter ?

JMX Exporter 利用 Java 的 JMX 机制来读取 JVM 运行时的一些监控数据,然后将其转换为 Prometheus 所认知的 metrics 格式,以便让 Prometheus 对其进行监控采集。

那么,JMX 又是什么呢?它的全称是:Java Management Extensions。 顾名思义,是管理 Java 的一种扩展框架,JMX Exporter 正是基于此框架来读取 JVM 的运行时状态的。

如何使用 JMX Exporter 暴露 JVM 监控指标 ?

下面介绍如何通过 JMX Exporter 来暴露 Java 应用的 JVM 监控指标。

JMX Exporter 的两种用法

JMX-Exporter 提供了两种用法:

  1. 启动独立进程。JVM 启动时指定参数,暴露 JMX 的 RMI 接口,JMX-Exporter 调用 RMI 获取 JVM 运行时状态数据,转换为 Prometheus metrics 格式,并暴露端口让 Prometheus 采集。
  2. JVM 进程内启动(in-process)。JVM 启动时指定参数,通过 javaagent 的形式运行 JMX-Exporter 的 jar 包,进程内读取 JVM 运行时状态数据,转换为 Prometheus metrics 格式,并暴露端口让 Prometheus 采集。

官方不推荐使用第一种方式,一方面配置复杂,另一方面因为它需要一个单独的进程,而这个进程本身的监控又成了新的问题,所以本文重点围绕第二种用法讲如何在 K8S 环境下使用 JMX Exporter 暴露 JVM 监控指标。

打包镜像

使用第二种用法,启动 JVM 时需要指定 JMX Exporter 的 jar 包文件和配置文件。jar 包是二进制文件,不好通过 configmap 挂载,配置文件我们几乎不需要修改,所以建议是直接将 JMX Exporter 的 jar 包和配置文件都打包到业务容器镜像中。

首先准备一个制作镜像的目录,放入 JMX Exporter 配置文件 prometheus-jmx-config.yaml:

---
ssl: false
lowercaseOutputName: false
lowercaseOutputLabelNames: false
: 更多配置项请参考官方文档。

然后准备 jar 包文件,可以在 jmx_exporter 的 Github 页面找到最新的 jar 包下载地址,下载到当前目录:

wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.13.0/jmx_prometheus_javaagent-0.13.0.jar

再准备 Dockerfile,这里以 tomcat 为例:

FROM tomcat:jdk8-openjdk-slim
ADD prometheus-jmx-config.yaml /prometheus-jmx-config.yaml
ADD jmx_prometheus_javaagent-0.13.0.jar /jmx_prometheus_javaagent-0.13.0.jar

最后编译镜像:

docker build . -t ccr.ccs.tencentyun.com/imroc/tomcat:jdk8

搞定!如果想要更简单,可以利用 docker 多阶段构建,省掉手动下载 jar 包的步骤,Dockerfile 示例:

FROM ubuntu:16.04 as jar
WORKDIR /
RUN apt-get update -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y wget
RUN wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.13.0/jmx_prometheus_javaagent-0.13.0.jar

FROM tomcat:jdk8-openjdk-slim
ADD prometheus-jmx-config.yaml /prometheus-jmx-config.yaml
COPY --from=jar /jmx_prometheus_javaagent-0.13.0.jar /jmx_prometheus_javaagent-0.13.0.jar

部署 Java 应用

有了打包好的镜像,下一步我们看下如何部署应用到 K8S,关键点在于如何修改 JVM 启动参数以便启动时加载 JMX Exporter。JVM 启动时会读取 JAVA_OPTS 环境变量,作为额外的启动参数,所以我们部署时可以为应用增加一下这个环境变量,示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tomcat
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: ccr.ccs.tencentyun.com/imroc/tomcat:jdk8
        env:
        - name: JAVA_OPTS
          value: "-javaagent:/jmx_prometheus_javaagent-0.13.0.jar=8088:/prometheus-jmx-config.yaml"

---
apiVersion: v1
kind: Service
metadata:
  name: tomcat
  labels:
    app: tomcat
spec:
  type: ClusterIP
  ports:
  - port: 8080
    protocol: TCP
    name: http
  - port: 8088
    protocol: TCP
    name: jmx-metrics
  selector:
    app: tomcat
  • 启动参数格式: -javaagent:<jar>=<port>:<config>
  • 这里使用了 8088 端口暴露 JVM 的监控指标,可自行更改。

添加 Prometheus 监控配置

暴露了 JVM 的监控指标,现在来配置下 Prometheus,让监控数据被采集到,配置示例:

    - job_name: tomcat
      scrape_interval: 5s
      kubernetes_sd_configs:
      - role: endpoints
        namespaces:
          names:
          - default
      relabel_configs:
      - action: keep
        source_labels:
        - __meta_kubernetes_service_label_app
        regex: tomcat
      - action: keep
        source_labels:
        - __meta_kubernetes_endpoint_port_name
        regex: jmx-metrics

如果是安装了 prometheus-operator,也可以通过创建 ServiceMonitor 的 CRD 对象来配置 Prometheus:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: tomcat
  namespace: default
  labels:
    app: tomcat
spec:
  endpoints:
  - port: jmx-metrics
    interval: 5s
  namespaceSelector:
    matchNames:
    - default
  selector:
    matchLabels:
      app: tomcat

添加 Grafana 监控面板

采集到了数据,下面看下如何展示数据,如果熟悉 Prometheus 和 Grafana,可自行根据指标设计需要的面板,社区也有提供现成的,不过都有些老了,部分视图可能展示不出来,用的比较多的是 https://grafana.com/grafana/d... ,可以直接导入,面板效果图:

img

参考资料

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 3 收藏 2 评论 0

高阳Sunny 赞了文章 · 10月13日

微服务治理实践:服务契约

简介: 随着微服务架构越来越流行,越来越多的公司使用微服务框架进行开发。甚至不止是公司,连笔者的研究生导师都要对实验室的Spring Boot工程项目转型使用微服务框架了。

本文是《微服务治理实践》系列篇的第四篇文章,主要分享Spring Cloud微服务框架下的服务契约。
第一篇:《微服务治理解密》
第二篇:《微服务治理实践:服务查询》
第三篇:《微服务治理实践:金丝雀发布》
在详细讲述服务契约之前,先给大家讲一个场景。

前言

随着微服务架构越来越流行,越来越多的公司使用微服务框架进行开发。甚至不止是公司,连笔者的研究生导师都要对实验室的Spring Boot工程项目转型使用微服务框架了。随着时间的推移,服务量逐渐上升,小学妹吃不消跑来问我问题:

一姐,我来交接你之前写的项目啦,你什么时间方便我想问你一些问题。这么多微服务接口,感觉不知道从哪里去看会比较好呢。

我想了想自己刚入门时候写的垃圾代码,还没有注释,无语凝噎。

好。我平时工作日在实习,周末给你讲哈。

于是到周末,花了整整一个晚上的时间,终于给零基础学妹从众多接口的含义,到参数列表的解析,最后到讲解百度应该搜什么关键词(我好南),全方位视频指导。学妹十分感动:

一姐你太贴心了555,跟别人协作项目的时候,经常能讲上几句就不错了,然后我还是什么都不明白,改完接口也不及时告诉我。还是你最好了,后面还有什么不懂的我再来问你哦。

从以上场景,我们可以总结出使用微服务框架后,会带来的几点进度协同问题:
1、不及时提供接口API:
尤其体现在项目交接上,该问题对人员变动比较频繁的组织,如高校项目的准毕业生和新生交接、企业项目的外包人员交接,问题会显得更加突出。开发人员经常过于关注微服务的内部实现,相对较少设计API接口。

程序员最讨厌的两件事:1. 写注释 2. 别人不写注释

是不是经常想着写完代码再写注释,但真正把代码写完以后,注释/接口描述一拖再拖最后就没有了?别告诉我你没有过。

2、不及时变更接口:
即使有了API文档,但由于文档的离线管理,微服务接口变更以后,文档却没有及时变更,影响协作人员的开发进度。

综上我们看到,我们不但希望所有的微服务接口都可以很方便的添加规范的接口描述,而且也能随着接口的变更及时更新文档。因此,我们需要服务契约来帮助我们解决这些问题。

为什么我们需要服务契约

首先我们来看服务契约的定义:

服务契约指基于OpenAPI规范的微服务接口描述,是微服务系统运行和治理的基础。

有人可能会问了,既然想要规范的描述接口,我有很多其他的方式啊,为什么我要用服务契约?

1、 我用Javadoc来描述接口然后生成文档不可以吗?
可以,但刚刚我们也提到了“程序员最讨厌的两件事”,要求所有的开发人员都去主动的按照规范写注释,把所有的接口、参数列表的类型、描述等信息全都写清楚,是一件比较费时费力的事情。我们希望有一个能够减少开发人员负担的方法。

2、 现在不是有很多专业的API管理工具吗,我直接用专业的API管理工具去维护也是可以的吧。
API管理工具我们也是有考虑的,但是有如下的问题:
• 很多工具依然缺少自动化的API生成;
• 不是专注于解决微服务领域的问题,随着服务量迅速上升,管理起来依旧比较困难。

3、 那微服务框架本身也会有提供相关的接口管理功能吧,Dubbo可以用Dubbo Admin,Spring Cloud可以用Spring Boot Admin,它们不香吗?

这里篇幅有限,我们不再去详细讲述开源工具我们怎么去一步步使用,就用一张表格说话:

image.png

从表格可以看到,EDAS微服务治理的服务契约,支持版本更广泛了,配置难度更低了,代码侵入性没有了,直接用EDAS的Agent方案,它不是更香了?

EDAS 服务契约实践

下面我们来体验一下,EDAS上如何查看Spring Cloud的微服务契约。

创建应用

根据你的需要,选择集群类型和应用运行环境,创建Provider和Consumer应用。

image.png

服务查询控制台

1、 登录EDAS控制台,在页面左上角选择地域;
2、 左侧导航栏选择:微服务治理 -> Spring Cloud / Dubbo / HSF -> 服务查询;
3、 服务查询页面单击某个服务的详情;

image.png

查看服务契约

服务详情页面包括基本信息、服务调用关系、接口元数据、元数据等信息。在“接口元数据”一栏,便可查看服务的API信息。当用户使用Swagger注解时,会在“描述”列显示相应信息。
image.png

服务契约实现细节

在设计服务契约功能的时候,我们不但解决了开源框架中配置难度大,且部分方案具有代码侵入性的问题,而且针对如下阶段的难点都做了相应的方案,相信这些地方也是微服务框架的使用者会关心的:

1、数据获取

• 获取的同时是否还需要其他配置?
• 如何获取所需的方法名及描述、参数列表及描述、返回类型等信息?
• 会不会影响服务的性能?
• 信息能不能全面的拿到?
• 能不能同步接口的变更?

2、数据解析

• 能不能看到参数类型/返回值类型的详细结构?
• 解析参数结构的时候会不会影响启动时间?
• 泛型、枚举是否支持?
• 循环引用如何解决?
下面我们来详细介绍一下这几点都是如何解决的。

数据获取

为了减少用户的配置和使用难度,我们采用了Agent方案,用户无需任何额外的代码和配置,就可以使用我们的微服务治理功能。

Java Agent是一种字节码增强技术,运行时插入我们的代码,便可稳定的享受到所有的增强功能。

而且通过测试可得,只要在SpringMVC的映射处理阶段,选取合适的拦截点,就可以获取到所有的方法映射信息,包括方法名、参数列表、返回值类型、注解信息。由于该点在应用启动过程中只发生一次,因此不会有性能的影响。

我们获取的注解主要是针对Swagger注解。作为OpenAPI规范的主要指定者,Swagger虽并非是唯一支持OpenAPI的工具,但也基本属于一种事实标准。注解解析的内容在表格的描述部分进行展示:
• Swagger2的注解解析(如@ApiOperation,@ApiParam,@ApiImplicitParam),解析value值在“描述”列显示;
• OpenAPI3的注解解析(如@Operation,@Parameter),解析description值在“描述”列显示。

当接口发生变更时,只要将新版本的应用部署上去,显示的服务契约信息就会是最新的,无需担心接口描述信息不能同步的问题。

数据解析

如果参数列表/返回值的类型是一个复杂类型,一般情况我们只看到一个类型名。那么有没有办法可以看到这个复杂类型的具体构成呢?

聪明的你可能就会想到,通过反射来递归遍历该类所有的Field,不就都解决了?思路确实如此,但实际要考虑的情况会更复杂一些。

image.png
以该复杂类型CartItem为例,它可能不但会包含基本类型,还可能会涉及到泛型、枚举,以及存在循环引用的情况。

因此在解析该类型之前,我们需要先判断一下该类型是否存在泛型、枚举的情况,如果是,需要额外解析并存储泛型列表及枚举列表。

而循环引用问题,我们只需借助一个typeCache即可解决。如下图,A和B构成了一个循环引用。
image.png
如果我们不采取任何措施,递归遍历将永远没有出口。但是,如果我们在遍历A的所有类型之前,先判断一下typeCache里是否存在TypeA。对TypeB也以此类推:
image.png
那么当遍历ObjB中所包含类型时,如果遇到了TypeA,同样也会先判断typeCache中是否存在。如存在,就无需再递归遍历ObjA中所有的类型了,而是直接记录一个A的引用。因此,循环引用问题也就得以解决。
image.png

最终的解析信息,可以在服务测试功能中得以体现。未来我们可能会支持直接在服务查询中的服务契约页,通过一个入口显示复杂类型的具体解析结构。

由此我们看到,在服务契约的获取及解析阶段,涉及到的可能影响用户体验的问题都得到了一定的解决。

不止是服务契约

本文介绍了几种接口描述方法,并且和开源框架的微服务接口管理功能进行对比,引出了EDAS服务契约。虽然服务契约看起来只是在控制台上的一个接口信息展示功能,但在未来的发展中不可或缺,其上报的关键信息可以很大程度的优化服务测试、服务鉴权、标签路由的体验,是微服务治理体系中的基础功能。

EDAS微服务治理在未来甚至还可以在服务契约的基础上增加更多的增强功能,欢迎体验。

除了 EDAS 和 MSE(微服务引擎)这些微服务产品之外,我们还有 ARMS (应用实时监控服务)、ACM(应用配置管理)、SAE(Serverless 应用引擎)等云产品。欢迎加入我们一起,用心服务客户,共同打造产品,让业务永远在线。

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 10月12日

从哲学源头思考自动驾驶网络架构设计

摘要:本篇从哲学的角度阐述自动驾驶网络架构设计的方法。

自动驾驶网络关键在架构创新,创新不是漫无边际,毫无逻辑和实现可能性的瞎想,没有约束和方法论的瞎想是民科干的事情。我们要通过坚实的架构设计方法,铺就一个通往愿景的路。

下面讲的方法论既是一种知识,也是一种技能。通过知识学习可以更好的理解架构设计技巧。但是作为技能,却需要不断磨练才能掌握。就像学游泳一样,没有知识你很难游好,但是你有了再多知识,进入水中一样不会游。

01 从哲学源头开始思考

在讲架构设计之前,先学习点逻辑知识。先用一个例子帮助大家理解一下什么是概念的内涵以及语境的知识。我们有时讨论问题经常整拧巴了,大家讨论不到一块去,很多时候是因为概念理解的不一样。讲个小故事,有一次我讲课时,说管理老外和管理中国人差不多,结果很多人反对,觉得我不了解老外,然后讲了很多老外不同的地方,我反问了一句,那中国人是不是管理方式就一样呢?当你从人的一般特点看老外的时候,其实大家都一样,都有七情六欲,都需要愿景,都需要尊重,都需要指导,都需要鞭策,这和中国人有啥不一样?人面对事情需要寻找共性才能高效处理。如果要寻找个性,那就麻烦了,这世界有完全一样的人吗?昨天的你和今天的你是一样的吗?

我说老外一样的时候显然是在讲一般原则,而对具体的事情处理上,不止老外,其实面对每个个体,每个个体的不同时刻,都要区分处理。而说话时到底是指“一般原则”还是“特殊原则”,这个隐含的背景信息就是语境,语境一般在对话中并不出现,而是靠沟通双方根据理解来自动补充。人对语境处理的能力非常强,但是对话有时语境理解会发生错误,这个就是平时所说的沟通不在一个频道上了。

软件开发要求逻辑非常清晰,特别是大型软件的架构设计更是如此,需要非常好的抽象思维能力,所以深入理解逻辑方法很重要。

在理解逻辑方法之前,首先要理解我们说的客观世界就是由实体、实体属性和关系三种要素确定的。明确这三种要素,事物就唯一确定了。而软件世界其实就是实体世界的映像,所以也是这三种要素。我画了一张简图,先看一下,后面会用到。

关于思维方法有比较、分类、分析、抽象、概括、综合几种方法,在传统的哲学中,“比较”方法一般和其他方法并列,其实不是这样的,比较是最基础的思维过程,是“分析”“抽象”等其他方法的基础。人是通过比较才能确定事物的边界的,然后根据边界进行划分类别,也就是“分类”。比如一堆杂粮你可以通过分析看出有黑豆,红豆,绿豆,薏米,莲子,小米,大米等等,这个过程就是你对图像进行了不断比较,找到图像的边缘,和记忆中图像比较确定杂粮种类。只是对图像的比较分析都是用神经网络一瞬间完成的,人没有心理意识,而对抽象事物的分析过程是可以知觉的,你回想一下,分析事物过程是不也这样,就是不停的比较各个情况。

所以分析就是通过不停的比较,根据比较结果对事物进行划分,分解出各种实体,实体属性和实体间关系。

哲学上“抽象”的方法理解也简单,实体不是有很多属性吗,比如人有人格特征,社会关系,生物等很多属性,生物属性下面还有形态和DNA等属性。抽象就是根据需要选择属性的过程,这种“需要”的理解有时就是双方的默契,如果估计沟通的时候没有默契就要明确抽象到什么程度。例如刚开始我讲了老外都是一样的,我就是选了老外的人格特征属性,而忽略了社会关系属性。我哪天说其实人和狗也一样,就是选了两种的生物属性的共性。要是哪天我说其实石头和人也是一样的,只要抽象到质子和中子一级就行了。我要说今天的我的昨天的我不一样也行,因为想法和年龄都变了嘛。

还是用一堆杂粮举例,通过分析这个筛子把黑豆,红豆,绿豆,薏米,莲子,小米,大米这些米豆都分开了。抽象的动作就像你认为小米和大米都不算粗粮,只留了薏米,莲子、各种豆子。但是也有另外的抽象方法,就是我只选可以解毒的粗粮,那就只抽象出绿豆了,其他就都是次要的了。所以抽象什么不是绝对的,而是根据需要进行的。

抽象往往和本质一词有关联,顺便说一下什么是事物本质属性。抽象一般是根据需要把没用的属性去掉,只留关键的属性是,例如简笔画为了区分梨和苹果,一般抽象到形状就能分出来了,这种抽象就够用了,但是如果哪天要画苹果梨(真有这种水果的),形状的抽象就不行了,所以显然形状不是苹果和梨的本质区别。那是什么是这两者的本质区别呢,口味显然也不行,我们现在可以改良品种搞成口味差不多。梨和苹果最本质的区别是他们的DNA不同。所以事物本质属性就是能抽象到用于对事物进行分类的最少特征属性。

总结一下,抽象就是根据需要选择属性和关系的思维过程。至于怎么选择属性和关系、需要到什么程度,主要看实际场景了,这个才是难点。就像画画一样,颜料,笔,纸都一样,理工科的你画的和徐悲鸿画的那价值可是差远了。你画的能被大家看到欣赏一下已经很不错了,徐悲鸿一张画就够一般人奋斗一辈子,这就是差别,是吧!

如果抽象的对象是像苹果一样的实物,你可以识别出画的苹果的,画的最抽象的情况下就是一个简笔画。如果把抽象结果和语言符号连接起来,就成了概念“苹果”这个词了。而如果抽象的对象是像文章这种本身也是抽象的和复杂的,那么这个过程我们可以称作概括方法。换一句话说,“概括”就是一种特殊的抽象。

刚刚讲的方法都是把事物分解开的方法,那么“综合”是反过来的过程,是把各个部分组织起来进行思维的方法。综合不是各个部分的简单相加,而是一种再加工过程,会产生从各个部分分别看无法生成的新知识来。不知道大家看油画时有没有注意到,你走进油画细看,就是一些色块,看不出啥东西来,但是走远一点,看到全貌的时候,一副栩栩如生的画就出现了,这个就是综合。综合思维在底层用到了“比较”和“顿悟”两个思维方法。

0 2业务建模方法

2.1 如何做业务抽象建模

理解了看起来哲学看起来很High的思维方法,我们开始讨论业务建模方法了,业务建模的本质就是对业务进行抽象和重构。先看一下建模过程简图,之后我再打开讲。

2.1.1 业务抽象与分类

实际业务建模过程可分为以下几个步骤:

第一个步骤就是堆砌材料,很多人理解业务有点不知道怎么入手,其实也很简单,就是到处收集材料,有几类:

  1. 已经有的框架,前人的智力成果肯定要参考的。
  2. 通用知识,可以帮助理解背景。
  3. 如下图的7P类资料,这是引用我以前写的一个业务调研框架。

这些材料拿到后先通读一遍,有个大概印象,等需要时再细读。

第二个步骤就是对业务功能进行抽象,确定思考最大的思考框架。比如自动驾驶网络需要哪些大的功能。

第三个步骤是在框架下分类,分类维度可以按照:时间,空间,人际,业务类型等分解。这个步骤先不用做细,大概分分,便于进一步进行思考。现存的业务功能一般都是互相交叉的,你中有我,我中有你。这种情况有时就是没想清楚引起的,有时是环境变化引起的。比如以前可以说西红柿是蔬菜,但是新的圣女果西红柿却变成水果了,严格讲就不能再说西红柿是蔬菜了。当存在这种交叉时就要进一步细分维度,只要足够细分维度,事情最终总是可以找到MECE(互相独立,完全穷尽)可分的维度。西红柿分类这个就比较简单了,以后给小朋友介绍时就变成传统西红柿是蔬菜和圣女果西红柿是水果了。这个过程最重要,一定要把所有交叉去掉。

第四个步骤,再进行抽象,把一些不关键的内容去掉。

上面这些过程要迭代好多次,反复提炼才行,经过这个过程一般就比较清晰了。

2.1.2 组件建模

抽象完之后下一步工作就是按最简洁原则对分类进行聚类,聚类要考虑将各类别统一到一个层次上,并对聚类进行命名便于管理。最后再识别一下各个聚类间关系,形成关系图。

还是用杂粮举例。我们开始会把分出来的黑豆,红豆,绿豆,薏米,莲子分别打包。但是这样分类比较多,卖的时候不好介绍。这时觉得黑豆,红豆,绿豆作用差不多,就再统一打成一个豆类的大包,和薏米、莲子包并列,就成了下面情况。黑豆,红豆,绿豆合并的过程就是一种聚类。这样介绍起来就清晰一点。

2.1.3 系统重构

上面分类打包完了如果要卖,显然要看看是不是和市场匹配了,如果不合适还要调整关系。比如豆子打包后发现绿豆有消毒的特殊功能,可以多卖钱,这个时候就要把绿豆拿出来单独卖。这个就是系统重构了。

2.2 业务抽象的技巧-角色扮演方法

当一个非常复杂的业务要做业务梳理时是困难的,关键是涉及的东西太多了,如果真要从各种细节了解起,那工作量是不可承受的,而且最后效果还不一定好。这个时候就可以使用角色扮演方法,这个也是通用的学习方法,可以高速的掌握一个领域的新知识。具体过程就是在了解事物前,不着急从实际入手,而是根据经验先自我设计一个框架,再根据假设框架去对照实际事物,如果一致就说明理解正确,如果不一致就找到原因,这样既可以快速了解业务,又可以发现现存的问题。这个是理解事物的一个非常快捷的方法。

2.3 业务梳理工具XMind

梳理业务肯定要有一个合适的工具。我经常用XMind。XMind可以导出各种漂亮的图,但是这个工具最大的作用是方便的进行属性和关系调整,对复杂的事情人一下子可能很难想清楚,所以可以把所有想法都列到工具里,然后慢慢进行逻辑调整,这样会保证效率。

0 3如何进行架构设计

3.1 架构设计过程

在设计架构前首先知道啥是架构,很多人一般把架构设计等同于软件架构设计,不过我这里把架构范围稍微扩大一点,把IT,流程,组织这类比较复杂的系统都纳入架构设计的范围,因为这三者往往是互相关联的。不过很遗憾的是,尽管很多人都谈架构,我却没有找到一个很好的架构定义。套用一句关于大数据的笑话,放在架构上也适用:

Architecture is like **age sex,everybody talks about it,nobody really knows what is it

本文借鉴TOGAF架构定义,重新进行了定义:

架构:是复杂系统组织形式的抽象描述,包括系统内部的组成模块,内部模块之间的关系及系统与环境之间的关系。

架构设计:是为了满足系统的业务使用需求,在业务价值空间、历史积累、架构发展的约束下,通过业务抽象、组件建模、系统重构方式构建架构,使系统的稳定性,灵活性,可演进性,成本实现具有最优解的过程。输出包括设计原则,架构和演化原则三个部分。

架构的设计的需求理解,业务建模方法在前面的小节中已经讲过了。下面再讲讲我对设计约束和架构师要求的理解。

架构不是凭空出来的,架构要考虑能不能实现和实现的代价,我刚刚买了一个智能音箱,发现音箱的音量调节逻辑很乱,我建议做音箱的兄弟把音量调节和使用场景绑定。这样从使用界面看最简单。但架构要不要这样做呢?架构师这个时候就要考虑关键点,因为音箱的音量可以在不同地方调节,如何保持各软件音量状态一致,就需要底层支持。他就一定要了解底层的实现能力,如果是以前的android版本,实现这个功能可能很困难,界面好用也得舍弃,而如何新的服务架构可能会支持,就值得试试,有困难也可以突破一下,所以架构设计一定是在充分理解系统能力的基础上的一种取舍。

还有架构设计也一定要考虑未来架构的稳定性,比如我们有的大型软件系统在服务化已经成为明显趋势的情况下还是采用了传统架构,干了几年后有不得不重新进行服务化设计。所以软件架构设计就要综合架构不同设计的收益大小,历史的积累情况,架构的未来发展几个因素综合考虑。

架构设计还是很复杂的,有时候就是一种艺术,需要各自平衡。如果想干架构师,那么有几个特点就不能少了。一个是开放,不能墨守成规,啥事看看老祖宗咋说,没自己的见解肯定不行。一个是要有洞察力,知道去粗存精,不能眉毛胡子一把抓,把架构越搞越复杂。业务也要精通,要善于学习,要知识多,知识越多考虑越全面。作为架构师不得不既懂业务,又懂软件。不然没法做出很好的设计。

架构设计师是非常关键的角色,往往决定了软件应用生死,承担如此之重的责任,大家会有疑问,那这么牛的人不是很难找?其实完全不用担心,架构设计说到底还是工程型问题,不像相对论除了天才谁也搞不了。这世界搞工程型问题的人才济济,不可能找不到的,只是看怎么找,给多少钱的问题。当然还有另外的担心,那成本会不会很高,其实也不用担心,架构师人数要求也很少,相对系统的成本并不高,所以苹果才会竭尽全力找最优秀的人才。

3.2 业界的架构设计方法

上面讲了最一般的架构设计的框架,下面这个ADM(Architecture Development Method)是TOGAF(The Open Group Architecture Framework)的企业架构设计方法,是The Open Group在美国国防部信息管理技术架构的基础上发布的,非常完善和详细,很值得学习。

现代知识搜索非常容易,如果知道哪些知识不知道,一搜索就查到了,关键是有时根本不知道自己哪些东西不知道。所以这里大家只要知道有这个很好的方法就行了,具体我就不讲了,有兴趣的话网上资料很多。

0 4架构设计应用示例

4.1 软件架构设计

我这里不讲具体的软件架构设计本身,着重讲一下软件架构设计的理念。从很流行的领域驱动设计方法(DDD:Domain-Driven Design)的理念看,本质上,业务软件设计就是用软件对现实业务的模拟,设计软件的过程就是对业务的理解过程。

DDD首先是一种设计思想,所谓思想就是回答“设计的本质是什么,主要逻辑是什么”这类大的问题。DDD强调要从业务视角思考怎么设计软件架构,设计一定要知道业务是什么样子的,业务的需求和问题是什么,有什么内在逻辑,而不是从软件技术本身出发设计,这个对设计而言就是大的方向问题。虽然这个方向说出来好像没什么,但是实践上很多软件人员更多是从软件本身开始设计,一遇到业务问题容易绕道走,所以强调从业务出发是这个方法最有价值的地方。

0 5架构参考设计

自动驾驶网络的参考设计,下面架构可以对比了解一下。

5.1 TOGAF EA和Frameworx

Frameworx是TMF的NOSS框架,相当于TOGAF EA的电信版实例。

5.2 TMF自治网络架构

下面是TMF的自治网络参考架构:

下面全文引自:中国电信《001-CTGMBOSS-OSS-2.5-概念体系分册(终审稿)》,这个文档比较老,但是现在问题依旧没变。

“业界对OSS的概念描述的比较清晰的TMF SID对概念的描述。在SID的体系中,包括了产品(Product)、服务(Service)、资源(Resource)三个主要概念,其中服务又细分成面向客户的服务CFS(Customer Facing Service)和面向资源的服务RFS(Resource Facing Servcie)。其中产品可以包含多个面向客户的服务、面向客户的服务由多个面向资源的服务组成,面向资源的服务由资源组成。具体关系如图所示。TMF在eTOM中对各个概念的定义原文如下:

Product is what an entity (supplier) offers or provides to another entity (customer). Product may include service, processed material, software or hardware or any combination thereof. A product may be tangible (e.g. goods) or intangible (e.g. concepts) or a combination thereof. However, a product ALWAYS includes a service component.

Services are developed by a Service Provider for sale within Products. The same service may be included in multiple products, packaged differently, with different pricing, etc.

Resources represent physical and non-physical components used to construct Services. They are drawn from the Application, Computing and Network domains, and include, for example, Network Elements, software, IT systems, and technology components.

本篇从哲学的角度阐述了架构设计的方法,下篇我将介绍一个我自己理解的网络运行功能的新ISOAP(我的香皂)模型,欢迎大家一起探讨。

点击关注,第一时间了解华为云新鲜技术~

查看原文

赞 1 收藏 0 评论 0