我们为线着色的方法

我们公司始终使用公认的实践(包括多线程问题)来努力提高代码的可维护性。 这不能解决不断增加的负载所带来的所有困难,但是可以简化支持-它还可以提高代码的可读性和开发新功能的速度。

现在,我们每天有47,000个用户,约有30台服务器在生产中,每秒有2,000个API请求以及每日发布。 Miro服务自2011年以来一直在开发,在当前实施中,用户请求由一组异构服务器并行处理。



竞争性访问控制子系统


我们产品的主要价值在于协作用户板,因此主要负担就落在他们身上。 控制大多数竞争性访问的主要子系统是董事会上用户会话的有状态系统。

对于其中一台服务器上的每个可打开的板,状态都会升高。 它既存储了确保协作和内容显示所需的应用程序运行时数据,又存储了系统数据(例如绑定到处理线程)。 只要服务器正在运行,并且板上至少有一个用户,有关状态存储在哪台服务器上的信息就会写入分布式结构中,并且对集群可用。 我们使用Hazelcast提供子系统的这一部分。 与该板的所有新连接都将以这种状态发送到服务器。

当连接到服务器时,用户进入接收流,其唯一的任务是将连接绑定到相应板的状态,流中将进行所有进一步的工作。

董事会与两个流相关联:网络,处理连接和负责业务逻辑的“业务”。 这使您可以将处理网络数据包和执行业务命令的异构任务的执行方式从串行转换为并行。 来自用户的已处理网络命令形成已应用的业务任务,并将其定向到业务流,并在其中进行顺序处理。 这样可以避免在开发应用程序代码时不必要的同步。

将代码分为业务/应用程序和系统是我们的内部惯例。 通过它,您可以区分作为用户功能和特征的代码,以及作为服务工具的​​通信,整理和存储的底层细节。

如果接收流检测到单板没有状态,则设置相应的初始化任务。 状态初始化由单独的线程类型处理。

任务的类型及其方向可以表示如下:



这样的实现使我们能够解决以下问题:

  1. 接收流中没有业务逻辑会减慢新连接的速度。 服务器上的这种线程存在于单个副本中,因此延迟会立即影响板的打开时间,并且如果业务代码中存在错误,则可以轻松地将其挂起。
  2. 状态初始化不会在板的业务流程中执行,并且不会影响来自用户的业务命令的处理时间。 这可能需要一些时间,并且业务流程会同时处理多个板,因此新板的开放不会直接影响现有板。
  3. 解析网络命令通常比直接执行它们要快,因此网络线程池的配置可能与业务线程池的配置不同,以便有效地使用系统资源。

流着色


上面在实现中描述的子系统非常重要。 开发人员必须牢记系统的方案,并考虑关闭电路板的反向过程。 关闭时,必须删除所有订阅,从注册表中删除条目,并在初始化它们的相同流中执行此操作。

我们注意到,此子系统中出现的错误和修改代码的困难通常与对执行上下文的了解不足有关。 杂乱的线程和任务使得很难回答在哪个特定线程中执行特定代码的问题。

为了解决此问题,我们使用了为线程着色的方法-这是旨在规范系统中线程使用情况的策略。 将颜色分配给线程,方法定义线程内执行的范围。 这里的颜色是一种抽象,它可以是任何实体,例如枚举。 在Java中,注释可以用作颜色标记语言:

@Color @IncompatibleColors @AnyColor @Grant @Revoke 

注释被添加到方法中,使用它们可以设置方法的有效性。 例如,如果方法的注释允许黄色和红色,则第一个线程可以调用该方法,而对于第二个线程,这样的调用将是错误的。



可以指定无效的颜色:



您可以在动态中添加和删除线程特权:



如下面的示例所示,缺少注释或注释表示该方法可以在任何线程中执行:



Android开发人员可能熟悉MainThread,UiThread,WorkerThread等注释的这种方法。

线程着色使用自记录代码的原理,该方法本身很适合进行静态分析。 使用静态分析,您可以在执行代码之前说出是否正确编写了代码。 如果我们排除Grant和Revoke批注,并假设初始化时的流已经具有不可更改的特权集,那么这将是对流不敏感的分析-静态分析的简单版本,不考虑调用顺序。

在实施流着色方法时,我们的devops基础结构中没有现成的静态分析解决方案,因此我们采用了更简单,更便宜的方法-我们引入了与每种流类型唯一关联的注释。 我们开始在运行时方面的帮助下检查它们的正确性。

 @Aspect public class ThreadAnnotationAspect { @Pointcut("if()") public static boolean isActive() { … //   ,    . , ,    } @Pointcut("execution(@ThreadAnnotation * *.*(..))") public static void annotatedMethod() { } @Around("isActive() && annotatedMethod()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Thread thread = Thread.currentThread(); Method method = ((MethodSignature) jp.getSignature()).getMethod(); ThreadAnnotation annotation = getThreadAnnotation(method); if (!annotationMatches(annotation, thread)) { throw new ThreadAnnotationMismatchException(method, thread); } return jp.proceed(); } } 

对于方面,我们使用aspectj库和maven插件,该插件在编译项目时提供编织功能。 编织最初配置为在使用ClassLoader加载类时加载时。 但是,我们面临这样一个事实,即在竞争性加载相同的类时,织布工有时表现不正确,结果是,类代码的原始字节保持不变。 结果,这导致非常不可预测的并且难以再现生产行为。 也许在当前版本的库中没有这样的问题。

方面的解决方案使我们能够快速找到代码中的大多数问题。

重要的是不要忘记始终保持最新状态:可以删除它们,增加懒惰度,可以完全关闭编织方面-在这种情况下,着色将很快失去其相关性和价值。

守卫者


着色的一种方式是java.util.concurrent中的GuardedBy批注。 它界定了对字段和方法的访问,指出了正确访问所需的锁。

 public class PrivateLock { private final Object lock = Object(); @GuardedBy (“lock”) Widget widget; void method() { synchronized (lock) { //Access or modify the state of widget } } } 

现代的IDE甚至支持对此注释的分析。 例如,如果代码有问题,IDEA将显示此消息:


为线程着色的方法并不新鲜,但是在Java之类的语言中,多线程访问经常用于可变对象,因此它不仅可以用作文档的一部分,而且可以在编译阶段使用汇编程序极大地简化多线程代码的开发。

我们仍然在各个方面使用实现。 如果您熟悉一个更优雅的解决方案或分析工具,该工具或分析工具可以提高此方法进行系统更改的稳定性,请在注释中分享它。

Source: https://habr.com/ru/post/zh-CN443666/


All Articles