利用Java可观察性进行有效编码

这些年来,当我试图使可观察性计划成功时,我看到了许多重复的常见错误。然而,这些组织障碍中最关键和最基本的是对技术和工具本身不可抗拒的迷恋。
在这里插入图片描述
这不应该令人惊讶。许多“让我们添加可观察性平台X”项目开始时声势浩大,但方向感非常模糊,成功标准极其混乱。有效的可观察性可以做些什么实际上有帮助令人怀疑的是,开发者工作得更好并没有出现在许多商业供应商和预言家的宣传中。问问你自己:你有多频繁地将目光从IDE中的代码上移开,以发现你可以从它的执行数据中了解到什么?

请不要误会我的意思,我非常相信可观察性(你的应用程序数据的别称)在软件开发中的作用。OpenTelemetry非常庞大。我可以清楚地看到它如何帮助开发人员编写更好的代码,引入新的范例,并加快开发周期。它可以激发开发人员提出他们从未考虑过的问题。然而,无论你在网上看到哪里,关注的焦点似乎仍然是可观察性本身、如何实现它以及如何开始。尽管闪亮的仪表板上有很酷的图形,但许多团队仍然不知道该从哪里开始。

在这篇文章中,我将尝试解决这个更有趣的话题:对于使用可观察性的开发人员来说,成功是什么样的?团队怎么能指望密码和释放;排放;发布更好地利用丰富的代码运行时数据?更重要的是,现在,可观察性可以告诉你关于你的代码的什么例子,以及它如何帮助你改进它?我们将查看具体的代码示例,了解如何在编码实践中利用可观察性。

【编辑:我应该提醒我的声明,关于在线内容,只关注可观察性教程。事实上,有一些伟大的内容马尔钦·格泽杰斯扎克、乔纳森·伊万诺夫和其他人写的文章,我强烈建议阅读这些文章,尽管它在更大程度上是一个例外】。

超越监控:开发中更短的反馈循环
可观察性的最大承诺是提供反馈——真实、客观的反馈,没有单元测试的一些偏差和偏见。想象一下,当您仍在处理代码更改时,会被提醒任何源于代码更改的倒退或问题。或者,始终了解您的代码的哪些部分在生产中实际使用,并根据集成测试结果轻松识别需要关注的薄弱点。

我认为这是开发人员可观察性的真正潜力,远远不是它作为“监控”解决方案的传统角色。监视器和警报至关重要,但不幸的是,它们的重点一直是报告已经发生的问题。也许是因为该技术主要由开发运维/SRE/IT团队使用,他们主要关注生产稳定性。

有一次,在这些疯狂的产品发布阶段中的一个阶段,我和团队中的其他开发人员感觉自己更像一个消防队而不是开发团队,我开玩笑地将我们疯狂的错误修复计划称为BDD——不是行为驱动的设计,而是错误驱动的开发。可悲的事实是,这种描述并非完全不准确。我们没有积极主动地改进代码,而是极度地反应的追逐一个又一个很快变得不可持续的问题。

让我们举一个更实际的例子
为了说明我们如何利用可观察性来改进我们的开发周期,让我们举一个现实场景中更实际的例子:团队中的高级开发人员Bob被要求向春天宠物诊所举例。似乎跟踪宠物的疫苗接种记录非常重要,Bob已经被要求与外部数据源集成来实现这一点。对于最初的MVP,Bob创建了一个特性分支,并继续实现一些新的功能。

Bob阅读了许多关于如何从Java应用程序中收集可观察性数据的教程,他有几个运行在后台的操作系统和免费工具来帮助他完成任务。在这篇文章的背景下,我不会详细介绍如何设置整个堆栈(因为它也被广泛记录),但如果有需要,我很乐意在后续文章中这样做。但是,您可以找到作为docker_compose文件的整个堆栈这里.

Bob的基本可观察性堆栈:

开放式遥测用于跟踪;他也有一个OTEL收藏者本地运行的容器,用于将数据路由到各种工具
贼鸥可视化痕迹
千分尺用于收集指标
普罗米修斯用于保存矩阵和OSS版本的格拉夫纳让它们可视化
需要注意的是,要开始用OTEL收集代码数据,鲍勃不需要代码有变化吗。在本地运行时,他可以安全地使用OTEL代理。在他的例子中,他只是在IDE的运行配置中引用代理,这样在本地运行/调试时就可以使用它。他还补充道docker-compose.override使用Docker/Podman启动应用程序时要使用的文件(这也不需要更改来源docker-compose文件;我写了这个巧妙的小技巧这里).
在这里插入图片描述
一切就绪后,Bob创建了一个新的功能分支,并开始开发新功能。您可以在中找到完整的分叉项目这回购,如果你想仔细看看代码。

疫苗API门面组件
幸运的是,有人已经为另一个模块编写了用于与模拟API通信的Spring组件。Bob的工作很简单:将组件注入到PetController中,并在添加宠物时使用它来检索数据。该组件非常简单,使用OKHttp库实现一个基本的REST调用来获取JSON格式的数据。

@WithSpan
    public VaccinnationRecord[] AllVaccines() throws JSONException, IOException {
        var vaccineListString = MakeHttpCall(VACCINES_RECORDS_URL);
        JSONArray jArr = new JSONArray(vaccineListString);
        var vaccinnationRecords =
            new ArrayList<VaccinnationRecord>();
        for (int i = 0; i < jArr.length(); i++) {
            VaccinnationRecord record = parseVaccinationRecord(jArr.getJSONObject(i));
            vaccinnationRecords.add(record);
        }
        return vaccinnationRecords.toArray(VaccinnationRecord[]::new);
    }
    @WithSpan
    public VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException {
        var idUrl = VACCINES_RECORDS_URL + "/" + vaccinationRecordId;
        var vaccineListString = MakeHttpCall(idUrl);
        JSONObject vaccineJson = new JSONObject(vaccineListString);
        return parseVaccinationRecord(vaccineJson);
    } 

更新Pet模型
接下来,为了保存疫苗接种数据而不是每次都检索它,必须更新模型和数据库结构。这涉及到很多样板文件,真的,但为了保存每只宠物的疫苗接种信息,这是必要的。Bob适时地添加了一个新表,在他的类中建立了关系模型,并更新了DDL脚本。

@Entity
@Table(name = "pet_vaccines")
public class PetVaccine extends BaseEntity {
    @Column(name = "vaccine_date")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate date;
    /**
     * Creates a new instance of Visit for the current date
     */
    public PetVaccine() {
    }
    public LocalDate getDate() {
        return this.date;
    }
    public void setDate(LocalDate date) {
        this.date = date;
    }
}

添加用于检索和更新新宠物疫苗接种日期字段的域服务
遵循最佳实践,Bob创建了一个简单的域服务,该服务将被注入到PetController中。新服务编排了域逻辑,用于从外部API检索新宠物的疫苗记录并使用最新日期更新模型。不幸的是,Bob也在这里犯了几个错误,其中一些与门面的抽象泄漏有关,该抽象隐藏了昂贵的HTTP调用。鲍勃也没有注意到很多逻辑是多余的。

@Component
public class PetVaccinationStatusService {
    @Autowired
    private PetVaccinationService adapter;
    public void UpdateVaccinationStatus(Pet[] pets){
        for (Pet pet: pets){
            try {
                var vaccinationRecords = this.adapter.AllVaccines();
                for (VaccinnationRecord record : vaccinationRecords){
                    var recordInfo = this.adapter.VaccineRecord(record.recordId());
                    if (recordInfo.petId()==pet.getId()){
                        PetVaccine petVaccine = new PetVaccine();
                        petVaccine.setDate(recordInfo.vaccineDate());
                        pet.addVaccine(petVaccine);
                    }
                }
            } catch (JSONException |IOException e) {
                //Fail silently
                Span.current().recordException(e);
            }
        }
    }
}

更新视图模板
最后,Bob添加了一个新字段,该字段将指示宠物疫苗是否过期。

..
<table class="table table-striped" th:object="${owner}">
      <tr>
        <th>Name</th>
        <td><b th:text="*{firstName + ' ' + lastName}"></b></td>
      </tr>
      <tr>
        <th>Address</th>
        <td th:text="*{address}"></td>
      </tr>
      <tr>
        <th>City</th>
        <td th:text="*{city}"></td>
      </tr>
      <tr>
        <th>Telephone</th>
        <td th:text="*{telephone}"></td>
      </tr>
      <tr>
        <th>Needs Vaccine</th>
        <td th:text="*{isVaccineExpired()}"></td>
      </tr>
    </table>
...

就是这样!变化已经准备好了。Bob甚至写了一些测试,看着它们变成一片绿色。Bob对快速的进展感到满意,并对代码在本地测试时运行正常充满信心,于是他转向收集的运行时数据,看看它能揭示出他的哪些变化。他决定扩大“完成”的定义并花费额外的精力检查与他的更改相关的数据。

救援的可观察性
首先,参考某种基线很重要。有两项API操作受到了变更的影响,Bob希望了解一下这些操作在实施变更前后的表现。作为可观察性设置的一部分,Bob还配置了千分尺和执行器,以提供有关API的有用指标(更多信息这里).这些可以通过actuator URL直接访问,在我们的例子中是http://localhost:8082/actuator/metrics。然而,为了更好的可视化和更多的图形选项,Bob将使用在他的堆栈中本地运行的Prometheus和Grafana OSS。

查看一些常见的Grafana仪表板,令人惊讶的是没有用于跟踪API响应时间的默认图表。可能是因为大多数仪表板都与Ops相关,关注CPU/RAM和堆大小,而不是开发人员的日常见解。幸运的是,使用执行器指标很容易配置这样一个仪表板。我们可以使用以下查询创建这样一个图,重点关注用于创建新宠物的API:

http_server_requests_seconds{uri="/owners/{ownerId}/pets/new", quantile="0.5", method="POST", outcome="REDIRECTION"} != 0

然后我们可以检查代码更改前后的图表。
之前:
在这里插入图片描述
之后:
在这里插入图片描述
呀!毫无疑问,这些变化导致了重大的性能问题。我们可以通过查看指标立即发现它,但是跟踪可以揭示更多关于根本原因和潜在问题的信息。是时候调用Jaeger了,它是我们可观测性堆栈的另一个组成部分。Jaeger习惯于将捕获的轨迹可视化,并在Bob忙于添加更多逻辑和功能时为他提供了调查其代码的机会:
在这里插入图片描述
因此,在没有添加一个断点的情况下,我们已经可以从这个请求中的代码中了解到很多信息。直到现在鲍勃都不知道的信息。虽然他在尝试新请求时确实注意到了一些滞后,但他并没有太在意。也许外部API只是速度慢?现在他已经可以访问跟踪了,他可以重新审视他正在引入的代码。

大量Select语句
第一个突出的问题是作为findById存储库方法的一部分触发了许多SQL语句。Spring数据会自动检测这一点,并为我们提供一些背景信息。更仔细地检查查询会发现一个熟悉的Hibernate缺陷:
在这里插入图片描述
看起来像是Visits在通常被称为N+1选择。有趣的是,这个问题似乎是PetClinic应用程序特有的,并且似乎早于Bob的更改。事实上,虽然这导致了一些速度下降,但并不像其他一些问题那样严重,这在Bob进一步检查跟踪时变得很明显。

HTTP请求聊天
性能下降的真正原因似乎与对Bob的误解有关,可能是由于对VaccineServiceFacade方法。他似乎不太清楚每次调用VaccineRecord已调用函数。如果有更好的命名约定,这种有漏洞的抽象可能会得到缓解,强调这实际上是一个长时间同步操作的执行。
在这里插入图片描述
在这里插入图片描述
隐藏的错误
HTTP请求发生了一些别的事情。当我们向下滚动请求列表时,Bob注意到其中一些请求以错误结束,随后在尝试序列化不存在的响应时出现异常。基于HTTP错误代码,根本原因与速率限制或节流或外部API有关。这个问题可以通过优化调用次数来暂时解决,但随着更多用户开始同时使用该组件,这个问题可能会再次出现。此外,这段代码中的异常处理肯定有问题,也许应该采用重试机制。
在这里插入图片描述
就在Bob开始纠正通过检查可观察性工件发现的许多问题之前,他决定快速查看一下他修改的其他API。在这种情况下,似乎没有明显的性能下降,但是检查跟踪仍然揭示了至少一个需要解决的问题。

墙上写着字
在数据中还可以识别出其他问题,但是让我们回顾一下我们的场景,考虑一下如果Bob没有在合并他的更改之前进行分析会发生什么:代码最终得到了部署。一些问题在CR或后期测试阶段被发现,导致更多的更改、额外的延迟和痛苦的合并,因为在此期间会有更多的更改。其他问题进入生产环境,导致进一步的灾难:减缓发布速度、匆忙修复补丁、增加团队的焦虑和挫败感等。毫无疑问,我们可以发现缩短反馈循环有很多好处。

大胜?不完全地
在这个有点天真的例子中,我们能够展示如何简单地打开OTEL并通过一些OSS工具传输数据,有可能为Bob和其他开发人员提供额外的护栏。然而,现实情况是Bob的团队很可能未能以可持续的方式继续应用此类反馈。出现这种情况有几个主要原因:

不连续的手动过程:整个实验依赖于Bob的敬业精神、自律和仔细检查代码的意愿。随着释放压力的增加,他越来越不可能这样做。尤其是在相当多的情况下,他花了时间调查数据,却没有发现任何有意义的东西。类似于测试,除非它是连续的和自动的,否则它很可能不会大规模发生。
专业知识要求如前所述,这个例子在强调一些明确的场景时有些做作。实际上,在没有统计、回归甚至基本ML知识的情况下,以这种方式处理数据以了解代码更改的影响是非常困难的。以我们检查的第一个图表“之前”状态为例。这两个值之间的差异代表侥幸、某种加速成本还是其他什么?
在这里插入图片描述

上下文切换和工具过载–上下文切换很难. 为了让这种编程范式发挥作用,它必须是一个可以拥有由工程团队完成。它不能是一堆开发人员需要掌握并知道如何正确阅读的仪表板和工具。我们越是减少所需的认知努力,这些信息就越有可能被应用。
在这里插入图片描述
未来是持续的反馈
持续反馈是一种新的开发实践,旨在弥合我们已经发现的差距:拥有大量易于收集的代码运行时数据,但需要手工工作、专业知识和时间来处理成实用和可操作的东西。有三个要素可以使其发挥作用:一个连续的管道(一个反向CI管道)、集成工具和ML/数据科学来自动化数据分析。

充分披露:我是迪格马的作者,我创建了一个免费的持续反馈插件,因为这种阻止开发人员使用代码数据的莫名其妙的鸿沟让我沮丧得发狂。我不止一次遇到过“Bob”场景,所有信息都在那里,就在那里。它可以在调试/测试数据中找到,甚至可以在关于代码的生产数据中找到,只是没有人会或可能检查它。

我们设想的是流水线自动化可以发现Bob最终发现的所有不同问题以及更多问题,并使其持续进行——这只是正常开发周期的一部分。事实上,我们从等式中删除了整个OTEL配置、样板文件和工具。将“开机”所需的工作减少到简单的按钮切换。通过这种方式,整个计划现在只需要Bob做两件事——启用可观察性和运行他的代码。这意味着更多的开发人员将能够开始探索代码运行时数据的潜力,而不仅仅是像Bob这样的顽固分子。
在这里插入图片描述
启用可观察性收集后,下面是Bob在本地调试和运行时使用Digma插件时看到的代码的IDE视图:
在这里插入图片描述
视图中的会话反模式、N+1查询、检测速度下降和隐藏错误等一切都成为开发人员视图的一部分,即动态文档。随着Bob继续编码、运行和调试,从收集的大量数据中不断解锁和解密。

通过这种方式,类似于测试,我们最终可以使可观察性变得透明——这不需要有意识的努力。就像管道工程一样,可观察性的作用应该是融入背景。数据是如何收集的,是OTEL还是其他技术都不重要。更重要的是,我们逆转了这个过程。Bob没有在大量的指标和跟踪中搜索与代码相关的问题,而是从查看代码问题开始,这些问题本身包含相关指标和跟踪的链接,以供进一步调查。
在这里插入图片描述
在考虑持续反馈时,最令人大开眼界的做法就是直接关掉它。令人抓狂的是,除了完全看不见之外,所有问题都依然存在——对我来说,这就像在黑暗中编码。

许多开发人员对我评论说,与采用测试类似,这种转变部分是技术上的,部分是文化上的。谁知道什么编码恐怖如果我们真的使用基于证据的指标来检验假设,会有多少假设会被发现或崩溃?也许有些人更喜欢在黑暗中编码?

在我看来,这只会让我们给代码库中的另一只野兽:技术债务赋予更多的形状和形式。理解延迟代码更改的差距、影响和系统范围的影响有望帮助推动更改并抵消许多组织遭受的一些向前倾斜的偏见。因此,尽管我是黑暗主题的大力支持者,我更喜欢在工作时关掉明亮的荧光灯,并且在工作时间绝对是一个夜猫子——我期待着在我代码的最黑暗的角落照亮一盏明亮的明灯。

就是这样!还有很多例子和细微差别可以作为未来博客文章的素材,我们几乎没有谈到使用CI/Prod数据的主题,这可能会产生巨大的影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值