CQ9节拍

实习生聚光灯: 2022 Software Engineering Projects
发表

2022年11月9日

Summer is intern season at CQ9; within a few short months, our interns are truly woven into the fabric of CQ9’s culture, adding vibrancy and enthusiasm to our offices around the globe. Working in close collaboration with a supportive network of mentors, 团队, 和长期CQ9ers, 实习生有机会解决自动化交易领域中一些最有趣的技术问题,并对我们的日常运营产生深远而持久的影响. 在过去的这个夏天, 实习生和CQ9ers通过一个排得满满的活动日程相互联系, 共享项目, and extra-long lunch lines in our cafeteria.

这篇文章是我们的实习生聚焦系列文章,重点介绍了三位软件工程实习生的工作:

  • 艾玛·杨的ToucanServer
  • Eric Zhang的cash S3集成
  • 作者:沙赫巴兹Momi

ToucanServer

作者:艾玛·杨

在过去的这个夏天, 作为核心实习生,我有机会了解到hrter正在解决的工程问题,并参与了我自己的一些项目. 我的一个项目是ToucanServer, 基于gRPC和protobuf的服务器,扩展了CQ9的手动交易(“点击交易”)平台.

这个问题

While most of CQ9’s trading is wholly automated, 随着公司的扩张,非自动化交易的数量也在增加. 为此目的, it has become increasingly important to support manual, 人为驱动的交易,允许hrter进行干预,并向市场发送订单和取消.

CQ9ers currently use Toucan, a command-line interface, for manual trading. 然而, as more and more 团队 at CQ9 need a way to manually enter orders, 我们越来越需要一个更通用的工具,可以适应不同团队在CQ9交易的方式.

ToucanServer

我的项目是开发一个gRPC服务器,将Toucan扩展为一个更灵活的手动交易平台,将客户(无论是半自动交易策略还是为其团队打开自定义前端接口的hrter)暴露给订单输入, 状态管理, and position-tracking features of Toucan.

在c++中实现, ToucanServer同步处理来自已连接客户端的一元和服务器流远程过程调用(rpc),以发送订单和取消.

客户端通过实例化gRPC存根与ToucanServer通信——gRPC存根是客户端程序中的一个本地对象,它允许客户端发送请求和接收响应,这些响应定义在一个protobuf规范中,使用gRPC服务器实现的方法. 这种客户机-服务器模型使得从ToucanServer发送订单和接收状态更新非常直观,便于用户为手工交易构建客户端, 因为客户端可以像调用本地方法一样对ToucanServer进行远程调用.

多线程在ToucanServer

构建ToucanServer的主要技术挑战之一是多线程的使用. 由于上下文切换的开销,大多数CQ9的基础结构都避免了多线程, 像ToucanServer这样的服务需要运行并发线程,以便服务器可以侦听和响应客户端请求. 在ToucanServer的案例中, since I used a synchronous server model, 当我们收到客户端请求时,该线程将阻塞,当服务器返回响应时,该线程将解除阻塞.

在用于订单输入的主线程和服务器线程之间进行通信, ToucanServer使用线程安全的请求队列,用于订单输入和来自客户端的状态响应. 将请求插入队列会用客户端特定的互斥锁阻塞服务器线程, 等待,直到订单输入线程处理完请求,并将结果传回客户端,然后再处理来自该客户端的任何其他请求.

The server handles a client request by queuing it in the thread-safe queue, which is picked up by the order entry thread
The order entry thread builds a response, 它将哪些内容放入线程安全队列中,以便服务器检索并返回给客户机

这种多线程设计不仅使我们能够与订单输入线程并发地运行同步服务器, 它还使我们能够将Toucan从单个客户端/进程的工具扩展为可以同时处理多个客户端的单个进程. 因为gRPC在它自己的线程池中自动复用客户端请求, ToucanServer可以处理并发客户端和并发客户端请求.

影响

ToucanServer的主要目的是使交易团队能够构建连接到ToucanServer的web应用程序和其他前端接口, 通过rpc发送指令, 并显示流到客户端的状态更新中包含的信息. 每个团队都可以根据对他们最有用的订单状态视图灵活地构建界面-这是我们手动交易平台的重要解锁.

cash S3集成

埃里克·张

对于高性能应用程序,数据需要靠近处理数据的地方. CQ9通过Cashy实现了这一点——Cashy是一种分布式缓存,可以为计算集群应用程序提供具有高可伸缩性的快速数据检索. cash利用了从本地内存到远程固态驱动器的层次结构, 中间有很多层, 并充当分布式文件系统的代理客户端,用于大规模读取.

为了发出请求,应用程序对同一台机器上的cash守护进程进行远程过程调用. That local Cashy process turns a single request into many block requests, which it sends to many other caching hosts in parallel. 最后,cash通过共享内存返回响应,从而允许它快速处理请求.

cash的默认配置通常用于为NFS读请求提供服务, 然而, 许多应用程序使用对象存储,比如亚马逊的简单存储服务(S3)。. 在这个项目中,我们扩展了cash以S3格式提供请求,并从S3存储读取数据. 这使得使用S3的应用程序能够在我们的计算集群中大规模运行.

Orange components are the proposed additions to Cashy

S3服务器

提供S3请求的一种简单方法是设置作为cash客户端的HTTP服务器,并将来自S3客户端的请求代理到cash. 然而, 由于额外的进程间通信,这会产生性能开销,并且需要额外的部署和监视.

Instead, we implement a subset of S3 functionality in Cashy itself. 因为Amazon S3 API很大, we start by only implementing the GetObject action, 哪个检索S3对象.

GET /文件.txt HTTP / 1.1
主持人:somebucket.somehost.com
日期:2022年10月3日(星期一)22:32 GMT
Authorization: authorization string

Amazon S3 GetObject请求

All other actions can be handled by a slower, but 全功能S3实现 without much performance loss.

Since Cashy operates in an asynchronous model, 所有网络连接都是使用由基于poll的事件循环控制的非阻塞套接字完成的. 要执行GetObject, cash worker进程首先要接收HTTP S3请求. This S3 request is then translated by an S3 parser into a Cashy request.

一旦cash接收到请求,它将对象读请求转换为多个块请求. 作为优化, 现金预取块,以便在客户端请求之前将它们预加载到更快的存储层. One tricky corner case is that for many S3 reads, the file size is unknown. 因此, we always prefetch at most the number of blocks that have already been read, to avoid prefetching more than twice the number of blocks necessary.

前面我们提到,除了GetObject之外,S3操作将由不同的, 全功能S3实现. 因此, the Cashy S3 server needs to redirect requests it can’t handle. 不管怎样 Amazon S3文档 S3客户端,如Boto3 do not properly support HTTP redirects. 为了解决这个问题,我们为Cashy S3服务器构建了代理S3请求的功能. 在这里,我们必须小心地重新验证请求,因为主机已经更改(参见 亚马逊签名V4).

S3读者

Now that applications can use the S3 protocol to read from Cashy, we’d also like Cashy to read from S3.

Our S3 reader needs to support the following interface:

   / / IReadCashyBlocks impl  ---------------------------------------------------
void beginReadBlock(BlockReadInfoPtr blockInfo)覆盖;

void cancelReadBlock(const BlockReadInfo& blockInfo)覆盖;

using CashyReadBlockCallback = Callback;
---------------------------------------------------

对于每个没有被取消的beginReadBlock,预期会有一个CashyReadBlockCallback.

为了有效地实现这一点, 每个Cashy worker都为每个S3主机提供一个连接池,Cashy可以从中检索信息. 请求在队列中等待,直到与相应主机的连接可用为止. 然后,我们发送请求,等待响应,并执行read回调. All of this is done asynchronously to avoid blocking.

Overall, the state machine roughly looks like:

S3读者的简化模型. 实线表示直接转换,虚线表示由控件管理的异步转换 epoll基于事件循环

结论

Before, Cashy could only support the following flow:

现在,Cashy支持客户端使用S3从Cashy的底层文件系统中读取数据. Moreover, existing Cashy clients can now read from S3 stores.

Given that Cashy can both serve and read from S3, we can also now use Cashy as a high-performance cache for S3 services!

Labshell

作者:沙赫巴兹Momi

假设您想要建立一个依赖于几十个配置文件的实时交易环境, 如果不是更多的话, binaries running across several hosts. Now imagine you want to interact with this environment, 实时交换这些二进制文件并动态更改配置. How would you approach this problem?

CQ9推出了自己的测试框架Labtest,试图解决这个问题, but it has some shortcomings; namely, it doesn’t have the interactivity desired as it only supports automated, 闭环测试. 我在CQ9暑期实习期间的第一个项目是开发Labshell, an interactive shell that aims to fill in this gap. 

体系结构

Labtest contains lots of features around deployment and configuration. 然而, it doesn’t provide any means of interactivity; it is a run-once style of program, and this comes with a few challenges, 特别是在建筑方面. While minor changes to the environment can be made (e.g. 重新启动进程), 诸如在不同主机上重新启动进程之类的重大更改需要破坏整个环境. 为了解释这一点, 我选择使用一个不可变的数据流架构,它带有一个向一个方向传递数据的中心渲染循环, allowing for effective handling of both types of changes.

Minor changes can be performed directly, 而主要的更改则由编排器在周期结束时进行整理和批量执行. 此处的不变性很重要,这样就不能修改当前运行的环境, but rather has to be evolved for a major change.

并发性

实现交互性, 我们需要一个单独的UI线程,它永远不会被阻塞,并准备好接收用户输入. 单独来看,这很容易通过创建一个新的后台线程来解决. 但是,我们还必须与运行后台作业的Labtest线程通信.g. sending keepalives to remote hosts, running processes, etc.) while managing cancellations and exceptions that occur in these jobs. Labtest内部采用的解决方案是使用Trio,这是一个用于异步并发的Python库. 使用Trio,您可以创建本质上是在单个线程上运行的协程的任务(以绕过GIL限制)。. We can leverage coroutines to have the small, Labtest需要调度的周期性任务在与UI线程相同的线程上运行, avoiding any concurrent read and write issues.

请注意,某些任务, 比如创造一个新的过程, 实际上是在内部创建新线程——trio通过创建线程安全的通信通道和管理线程的生命周期来为我们管理这一点.

用户界面

The user interface is perhaps the most important part of this project; how do we develop something efficient that people actually want to use? 在整个UI的设计和创建过程中,许多关键功能都促进了这一点. The UI framework I used to render the shell was prompt_toolkit, 一个优秀的库,它可以很容易地促进这些功能,同时也意识到上面提到的隐藏的挑战.

自动完成

自动完成 is probably the largest time saver, but with it comes a number of other questions, such as how to generate these completions, 如何订购补全, and how do they evolve as the user types in additional input?

推断法

If a user doesn’t have to type input, why should they? 这是另一个节省大量时间的方法,因为这意味着用户可以省略命令的许多参数.

# A command which accepts 2 parameters: command param_1 param_2
# param1 = ["option_1"], param2 = ["option_2", "option3"]
# All of these are valid since we can infer param_1:
命令option_2
option_1 option_3
# Invalid since we can't infer param2:
command
命令option_1

但它也有自己的考虑,比如识别合理的默认值, and addressing how to handle cases where multiple inferences are possible.

结论

最后, 我有一个shell,它可以在远程主机上启动bash和SSH会话, 重新配置活动环境, 重新启动和调试正在运行的进程, 启动预先配置到模拟交易环境的内部工具.

就像你看到的?

If you find the featured projects exciting, consider 应用!

不要错过任何一个节拍

关注我们,了解CQ9在工程、数学和自动化方面的最新信息.