<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rob Holland</title>
    <description>The latest articles on DEV Community by Rob Holland (@robholland).</description>
    <link>https://dev.to/robholland</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1102093%2Fafeec7f3-2d85-4935-8590-7bd2087e44c8.png</url>
      <title>DEV Community: Rob Holland</title>
      <link>https://dev.to/robholland</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robholland"/>
    <language>en</language>
    <item>
      <title>Tuning Temporal Server request latency on Kubernetes</title>
      <dc:creator>Rob Holland</dc:creator>
      <pubDate>Thu, 15 Jun 2023 16:44:17 +0000</pubDate>
      <link>https://dev.to/temporalio/tuning-temporal-server-request-latency-on-kubernetes-20np</link>
      <guid>https://dev.to/temporalio/tuning-temporal-server-request-latency-on-kubernetes-20np</guid>
      <description>&lt;p&gt;Request latency is an important indicator for the performance of Temporal Server. Temporal Cloud can offer reliably low request latencies, thanks to its custom persistence backend and expertly managed Temporal Server infrastructure. In this post, we’ll give you some tips for getting lower and more predictable request latencies, and making more efficient use of your nodes, when deploying a self-hosted Temporal Server on Kubernetes.&lt;/p&gt;

&lt;p&gt;When evaluating the performance of a Temporal Server deployment, we begin by looking at metrics for the request latencies your application, or workers, observe when communicating with Temporal Server. In order for the system as a whole to run efficiently and reliably, requests must be handled with consistent, low latencies. Low latencies allow us to get high throughput, and stable latencies avoid unexpected slowdowns in our application and allow us to monitor for performance degradation without triggering false alerts.&lt;/p&gt;

&lt;p&gt;For this post, we’ll use the &lt;a href="https://docs.temporal.io/clusters#history-service"&gt;History&lt;/a&gt; service as our example, which is the service responsible for handling calls to start a new workflow execution, or to update a workflow’s state (history) as it makes progress. None of these tips are specific to the History service—most of them can be applied to all the &lt;a href="https://docs.temporal.io/clusters#temporal-server"&gt;Temporal Server services&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The curious case of the unexpected throttling
&lt;/h2&gt;

&lt;p&gt;Generally, Kubernetes deployments will set CPU limits on containers to stop them from being able to consume too much CPU, starving other containers running on the same node. The way this is enforced is using something called &lt;a href="https://medium.com/@ramandumcs/cpu-throttling-unbundled-eae883e7e494"&gt;CPU throttling&lt;/a&gt;. Kubernetes converts the CPU limit you set on the container into a limit on CPU cycles per 1/10th second. If the container tries to use more than this limit, it is “throttled”, which means its execution is delayed. This can have a non-trivial impact on the performance of containers, as it can increase request latency. This is particularly true for requests requiring CPU intensive tasks, such as obtaining locks.&lt;/p&gt;

&lt;p&gt;For monitoring the Kubernetes clusters in our Scaling series (&lt;a href="https://dev.to/temporalio/scaling-temporal-the-basics-31l5"&gt;first post here&lt;/a&gt;) we use the &lt;a href="https://github.com/prometheus-operator/kube-prometheus#readme"&gt;&lt;code&gt;kube-prometheus&lt;/code&gt;&lt;/a&gt; stack.&lt;/p&gt;

&lt;p&gt;In contrast to the 1/10th second used to manage CPU throttling, the Prometheus system uses an interval of 15 seconds or more between scrapes of aggregated CPU metrics. The large difference in intervals between the throttling period and the monitoring scraping interval means that CPU throttling can be occurring even if CPU usage metrics are reporting a long way under 100% usage. For this reason, it’s important to monitor CPU throttling specifically.&lt;/p&gt;

&lt;p&gt;Here is an example for the History service:&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/3owwiacgyKBDDD52yfUhLd/0a66b3e514929ae4a57bd96d4829804d/CPU_Throttling-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/3owwiacgyKBDDD52yfUhLd/0a66b3e514929ae4a57bd96d4829804d/CPU_Throttling-mh.png" alt="History Service Dashboard: CPU is being throttled despite low CPU usage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We can see from the dashboard that although the history pods’ CPU usage is reporting below 60%, it is being throttled.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;kube-prometheus&lt;/code&gt; setups, you can use this Prometheus query to check for CPU throttling, adjusting the &lt;code&gt;namespace&lt;/code&gt; and &lt;code&gt;workload&lt;/code&gt; selectors as appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum(
    increase(container_cpu_cfs_throttled_periods_total{job="kubelet", metrics_path="/metrics/cadvisor", container!=""}[$__rate_interval])
    * on(namespace,pod)
    group_left(workload, workload_type) namespace_workload_pod:kube_pod_owner:relabel{namespace="temporal", workload="temporal-history"}
)
/
sum(
    increase(container_cpu_cfs_periods_total{job="kubelet", metrics_path="/metrics/cadvisor", container!=""}[$__rate_interval])
    * on(namespace,pod)
    group_left(workload, workload_type) namespace_workload_pod:kube_pod_owner:relabel{namespace="temporal", workload="temporal-history"}
) &amp;gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, how can we fix the throttling? Later we’ll discuss why you should probably stop using CPU limits entirely, but for now, as Temporal Server is written in Go, there is something else we can do to improve latencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  GOMAXPROCS in Kubernetes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GOMAXPROCS&lt;/code&gt; is a runtime setting for Go that controls how many processes it’s allowed to fork to provide concurrent processing. By default, Go will assume that it can fork a process for each core on the machine it’s running on, giving it a high level of concurrency.&lt;/p&gt;

&lt;p&gt;On a Kubernetes cluster, however, containers will generally not be allowed to use the majority of the cores on a node, due to CPU limits. This mismatch means that Go will make bad decisions about how many processes to fork, leading to inefficient CPU usage. It will (among other things) have to run garbage collection and other housekeeping tasks on CPU cores that it isn’t able to use for any useful amount of real work. As an example: on our Kubernetes cluster, the nodes have 8 cores, but our history pods are limited to 2 cores. This means they may create up to 8 processes, but across those 8 only be able to use a total of 2 cores' share of cycles in every throttling period. It then becomes easy for the container’s processes to starve each other of allowed CPU cycles. &lt;/p&gt;

&lt;p&gt;To fix this, we can let Go know how many cores it’s allowed to use by setting the &lt;code&gt;GOMAXPROCS&lt;/code&gt; environment variable to match our CPU limit. Note: &lt;code&gt;GOMAXPROCS&lt;/code&gt; must be an integer, so you should set it to the number of whole cores you set in the limit. Let’s see what happens when we set &lt;code&gt;GOMAXPROCS&lt;/code&gt; on our deployments:&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/3cwcbRAng6gzTZ2c4XpDL9/12585d12d60dff2258ec3d86cfd13e94/GOMAXPROCS-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/3cwcbRAng6gzTZ2c4XpDL9/12585d12d60dff2258ec3d86cfd13e94/GOMAXPROCS-mh.png" alt="History Dashboard: Showing reduced CPU usage and lower request latency after setting GOMAXPROCS"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the left of the graphs, you can see the performance with the default &lt;code&gt;GOMAXPROCS&lt;/code&gt; setting. Towards the right, you can see the results of setting the &lt;code&gt;GOMAXPROCS&lt;/code&gt; environment variable to “2”, letting Go know it should only use at most 2 processes. CPU throttling has gone entirely, which has helped make our latency more stable. We can also see that because Go can make better decisions about how many processes to create, our CPU usage has lowered, even though performance has actually improved slightly (request latency has lowered). Here, you can see how the CPU across all Temporal services drops after adjusting GOMAXPROCS:&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/4sVnSDGkT9lh2P4w2xy6n8/7f2233d25779c520992a5be4809ff8f3/Resources_-_Temporal_-_Dashboards_-_Grafana-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/4sVnSDGkT9lh2P4w2xy6n8/7f2233d25779c520992a5be4809ff8f3/Resources_-_Temporal_-_Dashboards_-_Grafana-mh.png" alt="Resource Dashboard: Showing reduced CPU by all Temporal Server services after setting GOMAXPROCS"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To help give a better experience out of the box, from release 1.21.0 onwards, Temporal will automatically set &lt;code&gt;GOMAXPROCS&lt;/code&gt; to match Kubernetes CPU limits if they are present and the &lt;code&gt;GOMAXPROCS&lt;/code&gt; environment variable is not already set. Before that release, you should manually set the &lt;code&gt;GOMAXPROCS&lt;/code&gt; environment variable for your Temporal Cluster deployments. Also note that &lt;code&gt;GOMAXPROCS&lt;/code&gt; will not automatically be set based on &lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container"&gt;CPU requests&lt;/a&gt;, only limits. If you are not using CPU limits, you should set &lt;code&gt;GOMAXPROCS&lt;/code&gt; manually to close to (equal or slightly greater) than your CPU request. This allows Go to make good decisions about CPU efficiency, taking your CPU requests into consideration.&lt;/p&gt;

&lt;p&gt;Which brings us nicely to our second suggestion…&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU limits probably do more harm than good
&lt;/h2&gt;

&lt;p&gt;Now that we’ve improved the efficiency of our CPU usage, I’m going to echo the &lt;a href="https://twitter.com/thockin/status/1134193838841401345?s=20"&gt;sentiment of Tim Hockin&lt;/a&gt; (of Kubernetes fame) and &lt;a href="https://home.robusta.dev/blog/stop-using-cpu-limits"&gt;many&lt;/a&gt; &lt;a href="https://medium.com/directeam/kubernetes-resources-under-the-hood-part-3-6ee7d6015965"&gt;others&lt;/a&gt; and suggest that you stop using CPU limits entirely. CPU requests should be closely monitored to ensure you are requesting a sensible amount of CPU for your containers, so that Kubernetes can make good decisions about how many pods it assigns to a node. This allows containers that are having a CPU burst to make use of any spare CPU on the node. Make sure to monitor node CPU usage as well—frequently running out of CPU on the node tells you that pods are bursting more often than your requests allow for, and you should re-examine their CPU requests.&lt;/p&gt;

&lt;p&gt;If you can’t disable limits entirely as they enforce some business requirements (customer isolation for example), then consider dedicating some nodes to the Temporal Cluster and use &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/"&gt;taints and tolerations&lt;/a&gt; to pin the deployments to those nodes. This allows you to remove CPU limits from your Temporal Cluster deployments while leaving them in place for your other workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoiding increased latency from re-balancing during Temporal upgrades
&lt;/h2&gt;

&lt;p&gt;Temporal Server’s &lt;a href="https://docs.temporal.io/clusters#history-service"&gt;History&lt;/a&gt; service automatically balances history shards across the available history pods, this is what allows Temporal Cluster to scale horizontally. &lt;em&gt;Note: Although we use the term balance here, Temporal does not guarantee that there will be an equal number of shards on each pod.&lt;/em&gt; The History service will rebalance shards every time a new history pod is added or removed, and this process can take a while to settle. Depending on the scale of your cluster, this rebalancing can increase the latency for requests, as a shard cannot be written to while it is being reassigned to a new history or pod. The effect of this will vary depending on what percentage of shards each of the pods is responsible for. The fewer pods you have, the greater the effect on latency when they are added/removed.&lt;/p&gt;

&lt;p&gt;The latency spike during a rollout can be mitigated in two ways, depending on the number of history pods you have:&lt;/p&gt;

&lt;p&gt;If you have more than 10 pods, the best option will be to do rollouts slowly, ideally one pod at a time. You can use low values for &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#max-surge"&gt;maxSurge&lt;/a&gt; and &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#max-unavailable"&gt;maxUnavailable&lt;/a&gt; to ensure pods are rotated slowly. Using &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#min-ready-seconds"&gt;minReadySeconds&lt;/a&gt;, or a &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#min-ready-seconds"&gt;startupProbe&lt;/a&gt; with initialDelaySeconds, can give Temporal Server time to rebalance as each pod is added.&lt;/p&gt;

&lt;p&gt;If you have less than 10 pods, it’s better to rotate pods quickly so that rebalancing can settle quickly. You will see latency spikes for each change, but the overall impact will be lower. You can experiment with the &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#max-surge"&gt;maxSurge&lt;/a&gt; and &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#max-unavailable"&gt;maxUnavailable&lt;/a&gt; settings to allow Kubernetes to roll out more pods at the same time. The defaults are 25% for each, which for 4 pods would mean only 1 pod will be rotated at once. Your mileage will vary based on scale and load, but we’ve had good success with 50% for maxSurge/maxUnavailable on low (4 or less) pod counts.&lt;/p&gt;

&lt;p&gt;Pull-based monitoring systems such as Prometheus use a discovery mechanism to find pods to scrape for metrics. As there is a delay between a pod being started and Prometheus being aware of it, the pod may not be scraped for a few intervals after starting up. This means metrics can report inaccurate values during a deployment, until all the new pods are being scraped.&lt;/p&gt;

&lt;p&gt;For this reason, it’s best to ensure you are not using metrics that are emitted by the History service when evaluating History deployment strategies. Instead, SDK metrics such as &lt;code&gt;StartWorkflowExecution&lt;/code&gt; request latency are a good fit here. Frontend metrics can also be useful, as long as the Frontend service is not being rolled out at the same time as the History service.&lt;/p&gt;

&lt;p&gt;These same deployment strategies are also useful for the &lt;a href="https://docs.temporal.io/clusters#matching-service"&gt;Matching&lt;/a&gt; service, which balances task queue partitions across matching pods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post we’ve discussed CPU throttling, CPU limits, and the effect of rebalancing during Temporal upgrades/rollouts. Hopefully, these tips will help you save some money on resources, by using less CPU, and improve the performance and reliability of your self-hosted Temporal Cluster.&lt;/p&gt;

&lt;p&gt;We hope you’ve found this useful, we’d love to discuss it further or answer any questions you might have. Please reach out with any questions or comments on the &lt;a href="https://community.temporal.io/"&gt;Community Forum&lt;/a&gt; or &lt;a href="https://t.mp/slack"&gt;Slack&lt;/a&gt;. My name is Rob Holland, feel free to reach out to me directly on &lt;a href="https://t.mp/slack"&gt;Temporal’s Slack&lt;/a&gt; if you like, would love to hear from you. You can also follow us on &lt;a href="https://twitter.com/temporalio"&gt;Twitter&lt;/a&gt; if you’d like more of this kind of content.&lt;/p&gt;

</description>
      <category>temporal</category>
      <category>docker</category>
      <category>kubernetes</category>
      <category>go</category>
    </item>
    <item>
      <title>Scaling Temporal: The Basics</title>
      <dc:creator>Rob Holland</dc:creator>
      <pubDate>Thu, 15 Jun 2023 16:42:55 +0000</pubDate>
      <link>https://dev.to/temporalio/scaling-temporal-the-basics-31l5</link>
      <guid>https://dev.to/temporalio/scaling-temporal-the-basics-31l5</guid>
      <description>&lt;p&gt;Scaling your own Temporal Cluster can be a complex subject because there are infinite variations on workload patterns, business goals, and operational goals. So, for this post, we will help make it simple and focus on metrics and terminology that can be used to discuss scaling a Temporal Cluster for any kind of workflow architecture.&lt;br&gt;
By far the simplest way to scale is to use Temporal Cloud. Our custom persistence layer and expertly managed Temporal Clusters can support extreme levels of load, and you pay only for what you use as you grow.&lt;br&gt;
In this post, we'll walk through a process for scaling a self-hosted instance of Temporal Cluster.&lt;/p&gt;

&lt;p&gt;Out of the box, our Temporal Cluster is configured with the development-level defaults. We’ll work through some &lt;strong&gt;load&lt;/strong&gt;, &lt;strong&gt;measure&lt;/strong&gt;, &lt;strong&gt;scale&lt;/strong&gt; iterations to move towards a production-level setup, touching on Kubernetes resource management, Temporal shard count configuration, and polling optimization. The process we’ll follow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load&lt;/strong&gt;: Set or adjust the level of load we want to test with. Normally, we’ll be increasing the load as we improve our configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt;: Check our monitoring to spot bottlenecks or problem areas under our new level of load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale&lt;/strong&gt;: Adjust Kubernetes or Temporal configuration to remove bottlenecks, ensuring we have safe headroom for CPU and memory usage. We may also need to adjust node or persistence instance sizes here, either to scale up for more load or scale things down to save costs if we have more headroom than we need.&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Our Cluster
&lt;/h2&gt;

&lt;p&gt;For our load testing we’ve deployed Temporal on Kubernetes, and we’re using MySQL for the persistence backend. The MySQL instance has 4 CPU cores and 32GB RAM, and each Temporal service (Frontend, History, Matching, and Worker) has 2 pods, with &lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"&gt;requests&lt;/a&gt; for 1 CPU core and 1GB RAM as a starting point. We’re not setting CPU limits for our pods—see our upcoming &lt;em&gt;Temporal on Kubernetes&lt;/em&gt; post for more details on why. For monitoring we’ll use Prometheus and Grafana, installed via the &lt;a href="https://github.com/prometheus-operator/kube-prometheus"&gt;kube-prometheus&lt;/a&gt; stack, giving us some useful Kubernetes metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/7jAvPG8jSHNbpI7WRFsEM3/614dcb36bf01a6816f470ded01b953f2/cluster.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/7jAvPG8jSHNbpI7WRFsEM3/614dcb36bf01a6816f470ded01b953f2/cluster.png" alt="Temporal Cluster diagram"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Scaling Up
&lt;/h2&gt;

&lt;p&gt;Our goal in this post will be to see what performance we can achieve while keeping our persistence database (MySQL in this case) at or below 80% CPU. Temporal is designed to be horizontally scalable, so it is almost always the case that it can be scaled to the point that the persistence backend becomes the bottleneck.&lt;/p&gt;
&lt;h3&gt;
  
  
  Load
&lt;/h3&gt;

&lt;p&gt;To create load on a Temporal Cluster, we need to start Workflows and have Workers to run them. To make it easy to set up load tests, we have packaged a simple Workflow and some Activities in the &lt;a href="https://github.com/temporalio/benchmark-workers/pkgs/container/benchmark-workers"&gt;benchmark-workers package&lt;/a&gt;. Running a &lt;code&gt;benchmark-worker&lt;/code&gt; container will bring up a load test Worker with default Temporal Go SDK settings. The only configuration it needs out of the box is the host and port for the Temporal Frontend service.&lt;/p&gt;

&lt;p&gt;To run a benchmark Worker with default settings we can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run benchmark-worker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image&lt;/span&gt; ghcr.io/temporalio/benchmark-workers:main &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image-pull-policy&lt;/span&gt; Always &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"TEMPORAL_GRPC_ENDPOINT=temporal-frontend.temporal:7233"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once our Workers are running, we need something to start Workflows in a predictable way. The benchmark-workers package includes a runner that starts a configurable number of Workflows in parallel, starting a new execution each time one of the Workflows completes. This gives us a simple dial to increase load, by increasing the number of parallel Workflows that will be running at any given time.&lt;/p&gt;

&lt;p&gt;To run a benchmark runner we can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run benchmark-worker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image&lt;/span&gt; ghcr.io/temporalio/benchmark-workers:main &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image-pull-policy&lt;/span&gt; Always &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"TEMPORAL_GRPC_ENDPOINT=temporal-frontend.temporal:7233"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; runner &lt;span class="nt"&gt;-t&lt;/span&gt; ExecuteActivity &lt;span class="s1"&gt;'{ "Count": 3, "Activity": "Echo", "Input": { "Message": "test" } }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our load test, we’ll use a deployment rather than &lt;code&gt;kubectl&lt;/code&gt; to deploy the Workers and runner. This allows us to easily scale the Workers and collect metrics from them via Prometheus. We’ll use a deployment similar to the example here: &lt;a href="https://github.com/temporalio/benchmark-workers/blob/main/deployment.yaml"&gt;github.com/temporalio/benchmark-workers/blob/main/deployment.yaml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this test we’ll start off with the default runner settings, which will keep 10 parallel Workflows executions active. You can find details of the available configuration options in the &lt;a href="https://github.com/temporalio/benchmark-workers#readme"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure
&lt;/h3&gt;

&lt;p&gt;When deciding how to measure system performance under load, the first metric that might come to mind is the number of Workflows completed per second. However, Workflows in Temporal can vary enormously between different use cases, so this turns out to not be a very useful metric. A load test using a Workflow which just runs one Activity might produce a relatively high result compared to a system running a batch processing Workflow which calls hundreds of Activities. For this reason, we use a metric called &lt;a href="https://docs.temporal.io/workflows#state-transition"&gt;&lt;strong&gt;State Transitions&lt;/strong&gt;&lt;/a&gt; as our measure of performance. State Transitions represent Temporal writing to its persistence backend, which is a reasonable proxy of how much work Temporal itself is doing to ensure your executions are durable. Using State Transitions per second allows us to compare numbers across different workloads. Using Prometheus, you can monitor State Transitions with the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum(rate(state_transition_count_count{namespace="default"}[1m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have State Transitions per second as our throughput metric, we need to qualify it with some other metrics for business or operational goals (commonly called Service Level Objectives or SLOs). The values you decide on for a production SLO will vary. To start our load tests, we are going to work on handling a fixed level of load (as opposed to spiky) and expect a StartWorkflowExecution request latency of less than 150ms. If a load test can run within our StartWorkflowExecution latency SLO, we’ll consider that the cluster can handle the load. As we progress we’ll add other SLOs to help us decide if the cluster can be scaled to handle higher load, or to more efficiently handle the current load.&lt;/p&gt;

&lt;p&gt;We can add a Prometheus alert to make sure we are meeting our SLO. We’re only concerned about &lt;code&gt;StartWorkflowExecution&lt;/code&gt; requests for now, so we filter the operation metric tag to focus on those.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TemporalRequestLatencyHigh&lt;/span&gt;
     &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Temporal {{ $labels.operation }} request latency is currently {{ $value | humanize }}, outside of SLO 150ms.&lt;/span&gt;
       &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Temporal request latency is too high.&lt;/span&gt;
     &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;histogram_quantile(0.95, sum by (le, operation) (rate(temporal_request_latency_bucket{job="benchmark-monitoring",operation="StartWorkflowExecution"}[5m])))&lt;/span&gt;
       &lt;span class="s"&gt;&amp;gt; 0.150&lt;/span&gt;
     &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
     &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
       &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking our dashboard, we can see that unfortunately our alert is already firing, telling us we’re failing our SLO for request latency.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/1vuDxyXumf9Iq9DZAs3p8n/780a082dc9749902cfa6497019472b30/Scaling__1-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/1vuDxyXumf9Iq9DZAs3p8n/780a082dc9749902cfa6497019472b30/Scaling__1-mh.png" alt="SLO Dashboard: Showing alert firing for request latency"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale
&lt;/h3&gt;

&lt;p&gt;Obviously, this is not where we want to leave things, so let’s find out why our request latency is so high. The request we’re concerned with is the &lt;code&gt;StartWorkflowExecution&lt;/code&gt; request, which is handled by the History service. Before we dig into where the bottleneck might be, we should introduce one of the main tuning aspects of Temporal performance, &lt;strong&gt;&lt;a href="https://docs.temporal.io/clusters/#history-shard"&gt;History Shards&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Temporal uses shards (partitions) to divide responsibility for a Namespace’s Workflow histories amongst History pods, each of which will manage a set of the shards. Each Workflow history will belong to a single shard, and each shard will be managed by a single History pod. Before a Workflow history can be created or updated, there is a shard lock that must be obtained. This needs to be a very fast operation so that Workflow histories can be created and updated efficiently. Temporal allows you to choose the number of shards to partition across. The larger the shard count, the less lock contention there is, as each shard will own fewer histories, so there will be less waiting to obtain the lock.&lt;/p&gt;

&lt;p&gt;We can measure the latency for obtaining the shard lock in Prometheus using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;histogram_quantile(0.95, sum by (le)(rate(lock_latency_bucket[1m])))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/7hNeLRnXBctLK7erfMfpeV/4232591701cb604cd2e72d24cb3453e1/Scaling__2-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/7hNeLRnXBctLK7erfMfpeV/4232591701cb604cd2e72d24cb3453e1/Scaling__2-mh.png" alt="History Dashboard: High shard lock latency"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at the History dashboard we can see that shard lock latency p95 is nearly 50ms. This is much higher than we’d like. For good performance we’d expect shard lock latency to be less than 5ms, ideally around 1ms. This tells us that we probably have too few shards.&lt;/p&gt;

&lt;p&gt;The shard count on our cluster is set to the development default, which is 4. Temporal recommends that small production clusters use 512 shards. To give an idea of scale, it is rare for even large Temporal clusters to go beyond 4,096 shards.&lt;/p&gt;

&lt;p&gt;The downside to increasing the shard count is that each shard requires resources to manage. An overly large shard count wastes CPU and Memory on History pods; each shard also has its own task processing queues, which puts extra pressure on the persistence database. &lt;em&gt;One thing to note about shard count in Temporal is that it is the one configuration setting which cannot (currently) be changed after the cluster is built&lt;/em&gt;. For this reason it’s very important to do your own load testing or research to determine what a sensible shard count would be, &lt;strong&gt;before&lt;/strong&gt; building a production cluster. In future we hope to make the shard count adjustable. As this is just a test cluster, we can rebuild it with a shard count of 512.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/1ACDp4CIlgckjgE4Pqidp5/c855c8f71ecfa25719b2822186f591ea/Scaling__3-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/1ACDp4CIlgckjgE4Pqidp5/c855c8f71ecfa25719b2822186f591ea/Scaling__3-mh.png" alt="History Dashboard: Shard latency dropped, but pod memory climbing"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After changing the shard count, the shard lock latency has dropped from around 50ms to around 1ms. That’s a huge improvement!&lt;/p&gt;

&lt;p&gt;However, as we mentioned, each shard needs management. Part of the management includes a cache of Workflow histories for that shard. We can see the History pods’ memory usage is rising quickly. If the pods run out of memory, Kubernetes will terminate and restart them (OOMKilled). This causes Temporal to rebalance the shards onto the remaining History pod(s), only to then rebalance again once the new History pod comes up. Each time you make a scaling change, be sure to check that all Temporal pods are still within their CPU and memory requests—pods frequently being restarted is very bad for performance! To fix this, we can bump the memory limits for the History containers. Currently, it is hard to estimate the amount of memory a History pod is going to use because the limits are not set per host, or even in MB, but rather as a number of cache entries to store. There is work to improve this: &lt;a href="https://github.com/temporalio/temporal/issues/2941"&gt;github.com/temporalio/temporal/issues/2941&lt;/a&gt;. For now, we’ll set the History memory limit to 8GB and keep an eye on them—we can always raise it later if we find the pod needs more.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/lFR4cS7n4rnQcFnWnba3a/58833e80b80b65ba0691153894e9b715/Scaling__4-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/lFR4cS7n4rnQcFnWnba3a/58833e80b80b65ba0691153894e9b715/Scaling__4-mh.png" alt="History Dashboard: History pods with memory headroom"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this change, the History pods are looking good. Now that things are stable, let’s see what impact our changes have had on the State Transitions and our SLO.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/61sgBu7aq8c8jMMMJRYcvN/dbd1d3e3232f5d79dfb21287c5250af0/Scaling__6-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/61sgBu7aq8c8jMMMJRYcvN/dbd1d3e3232f5d79dfb21287c5250af0/Scaling__6-mh.png" alt="History Dashboard: State transitions up, latency within SLO"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;State Transitions are up from our starting point of 150/s to 395/s and we’re way below our SLO of 150ms for request latency, staying under 50ms, so that’s great! We’ve completed a &lt;strong&gt;load&lt;/strong&gt;, &lt;strong&gt;measure&lt;/strong&gt;, &lt;strong&gt;scale&lt;/strong&gt; iteration and everything looks stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two!
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Load
&lt;/h3&gt;

&lt;p&gt;After our shard adjustment, we’re stable, so let’s iterate again. We’ll increase the load to 20 parallel workflows.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/5jaByfR0fEJEff8Fa2Gvfk/269b4da15595d06fdd714bda6689c97e/Scaling__7-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/5jaByfR0fEJEff8Fa2Gvfk/269b4da15595d06fdd714bda6689c97e/Scaling__7-mh.png" alt="SLO Dashboard: State transitions up"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checking our SLO dashboard, we can see the State Transitions have risen to 680/s. Our request latency is still fine, let’s bump the load to 30 parallel workflows and see if we get more State Transitions for free.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/5pds6aSQF43gP7JQlHkEzp/34fbda176b4558d4d76fde3bc41ac64a/Scaling__8-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/5pds6aSQF43gP7JQlHkEzp/34fbda176b4558d4d76fde3bc41ac64a/Scaling__8-mh.png" alt="SLO Dashboard: State transitions up"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see we did get another raise in State Transitions, although not as dramatic. Time to check dashboards again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure
&lt;/h3&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/6U4GgTbHiDun0u9AhNiMoS/283a92144e3c31e97c422280583517a1/Scaling__9-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/6U4GgTbHiDun0u9AhNiMoS/283a92144e3c31e97c422280583517a1/Scaling__9-mh.png" alt="History Dashboard: High CPU"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;History CPU is now exceeding its requests at times—we’d like to aim to have some headroom. Ideally, the majority of the time the process should use under 80% of its request, so let’s bump the History pods to 2 cores.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/5vXteyowBCu5yEGfJMTmdf/4f0df0946a8528e747a4b82504932b59/Scaling__10-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/5vXteyowBCu5yEGfJMTmdf/4f0df0946a8528e747a4b82504932b59/Scaling__10-mh.png" alt="History Dashboard: CPU has headroom"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;History CPU is looking better now, how about our State Transitions?&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/4svsTIEvMoFk8p1ZHuUtEF/da30c588d18dee3f2e5b027d56462572/Scaling__11-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/4svsTIEvMoFk8p1ZHuUtEF/da30c588d18dee3f2e5b027d56462572/Scaling__11-mh.png" alt="SLO Dashboard: State transitions up, request latency down"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re doing well! State Transitions are now up to 1,200/s and request latency is back down to 50ms. We’ve got the hang of the History scaling process, so let’s move on to look at another core Temporal sub-system, polling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale
&lt;/h3&gt;

&lt;p&gt;While the History service is concerned with shuttling event histories to and from the persistence backend, the polling system (known as the Matching service) is responsible for matching tasks to your application workers efficiently.&lt;/p&gt;

&lt;p&gt;If your Worker replica count and poller configuration are not optimized, then there will be a delay between the time a task is requested and when it is processed. This is known as Schedule-to-Start latency, and this will be our next SLO. We’ll aim for 150ms like we do for our Request Latency SLO.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TemporalWorkflowTaskScheduleToStartLatencyHigh&lt;/span&gt;
     &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Temporal Workflow Task Schedule to Start latency is currently {{ $value | humanize }}, outside of SLO 150ms.&lt;/span&gt;
       &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Temporal Workflow Task Schedule to Start latency is too high.&lt;/span&gt;
     &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;histogram_quantile(0.95, sum by (le) (rate(temporal_workflow_task_schedule_to_start_latency_bucket{namespace="default"}[5m])))&lt;/span&gt;
       &lt;span class="s"&gt;&amp;gt; 0.150&lt;/span&gt;
     &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
     &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
       &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TemporalActivityScheduleToStartLatencyHigh&lt;/span&gt;
     &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Temporal Activity Schedule to Start latency is currently {{ $value | humanize }}, outside of SLO 150ms.&lt;/span&gt;
       &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Temporal Activity Schedule to Start latency is too high.&lt;/span&gt;
     &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;histogram_quantile(0.95, sum by (le) (rate(temporal_activity_schedule_to_start_latency_bucket{namespace="default"}[5m])))&lt;/span&gt;
       &lt;span class="s"&gt;&amp;gt; 0.150&lt;/span&gt;
     &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
     &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
       &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding these alerts, let’s check out the polling dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/3rZ9bGWgJSt10SSx5CZNsT/d3ae7acf9613980fc537a4304b458e1c/Scaling__12-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/3rZ9bGWgJSt10SSx5CZNsT/d3ae7acf9613980fc537a4304b458e1c/Scaling__12-mh.png" alt="Polling Dashboard: Activity Schedule-to-Start latency is outside of our SLO"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So we can see here that our Schedule-to-Start latency for Activities is too high. We’re taking over 150 ms to begin an Activity after it’s been scheduled. The dashboard also shows another polling related metric which we call &lt;strong&gt;Poll Sync Rate&lt;/strong&gt;. In an ideal world, when a Worker’s poller requests some work, the Matching service can hand it a task from its memory. This is known as “sync match”, short for synchronous matching. If the Matching service has a task in its memory too long, because it has not been able to hand out work quickly enough, the task is flushed to the persistence database. Tasks that were sent to the persistence database needed to be loaded back again later to hand to pollers (async matching). Compared with sync matching, async matching increases the load on the persistence database and is a lot less efficient. The ideal, then, is to have enough pollers to quickly consume all the tasks that land on a task queue. To have the least load on the persistence database and the highest throughput of tasks on a task queue, we should aim for both Workflow and Activity Poll Sync Rates to be 99% or higher. Improving the Poll Sync Rate will also improve the Schedule-to-Start Latency, as Workers will be able to receive the tasks more quickly.&lt;/p&gt;

&lt;p&gt;We can measure the Poll Sync Rate in Prometheus using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum by (task_type) (rate(poll_success_sync[1m])) / sum by (task_type) (rate(poll_success[1m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to improve the Poll Sync Rate, we adjust the number of Worker pods and their poller configuration. In our setup we currently have only 2 Worker pods, configured to have 10 Activity pollers and 10 Workflow pollers. Let’s up that to 20 pollers of each kind.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/1eNCqEDaL8YpOzh9HZg01p/6a9ef9b984140cbdd22178ed41f6b411/Scaling__13-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/1eNCqEDaL8YpOzh9HZg01p/6a9ef9b984140cbdd22178ed41f6b411/Scaling__13-mh.png" alt="Polling Dashboard: Activity Schedule-to-Start latency improved, but still over SLO"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Better, but not enough. Let’s try 100 of each type.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/4TVpVof4j52n6ZJZwMA3pg/61072500d1a914c33baea56f61a37944/Scaling__14-mh__1_.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/4TVpVof4j52n6ZJZwMA3pg/61072500d1a914c33baea56f61a37944/Scaling__14-mh__1_.png" alt="Polling Dashboard: Activity Schedule-to-Start latency within SLO"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Much better! Activity Poll Sync Rate is still not quite sticking at 99% though, bump Activity pollers to 150 didn’t fix it either. Let’s try adding 2 more Worker pods…&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/3HtIYQStrxDiUaTvYo8cwP/2b23174471ac07ad60a4d7c6a8a65d23/Scaling__15-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/3HtIYQStrxDiUaTvYo8cwP/2b23174471ac07ad60a4d7c6a8a65d23/Scaling__15-mh.png" alt="Polling Dashboard: Poll Sync Rate &amp;gt; 99%"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nice, consistently above 99% for Poll Sync Rate for both Workflow and Activity now. A quick check of the Matching dashboard shows that the Matching pods are well within CPU and Memory requests, so we’re looking stable. Now let’s see how we’re doing for State Transitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/3iTC2pgTlHcCdjgY6MHTLj/8bfe80c9d541ae0f21fb5750468aacd6/Scaling__16-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/3iTC2pgTlHcCdjgY6MHTLj/8bfe80c9d541ae0f21fb5750468aacd6/Scaling__16-mh.png" alt="SLO Dashboard: State Transitions up to 1,350/second"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking good. Improving our polling efficiency has increased our State Transitions by around 150/second. One last check to see if we’re still within our persistence database CPU target of below 80%.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/0uuz8ydxyd9p/3j3qAqFod91T0wLTAau2iN/0ac24573769a3f6b974bd2c37e960b7c/Scaling__17-mh.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/0uuz8ydxyd9p/3j3qAqFod91T0wLTAau2iN/0ac24573769a3f6b974bd2c37e960b7c/Scaling__17-mh.png" alt="Persistence Dashboard: Database CPU &amp;lt; 80%"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes! We’re nearly spot on, averaging around 79%. That brings us to the end of our second &lt;strong&gt;load&lt;/strong&gt;, &lt;strong&gt;measure&lt;/strong&gt;, &lt;strong&gt;scale&lt;/strong&gt; iteration. The next step would either be to increase the database instance size and continue iterating to scale up, or if we’ve hit our desired performance target, we can instead check resource usage and reduce them where appropriate. This allows us to save some costs by potentially reducing node count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We’ve taken a cluster configured to the development default settings and scaled it from 150 to 1,350 State Transitions/second. To achieve this, we increased the shard count from 4 to 512, increased the History pod CPU and Memory requests, and adjusted our Worker replica count and poller configuration.&lt;/p&gt;

&lt;p&gt;We hope you’ve found this useful. We’d love to discuss it further or answer any questions you might have. Please reach out with any questions or comments on the &lt;a href="https://community.temporal.io/"&gt;Community Forum&lt;/a&gt; or &lt;a href="https://t.mp/slack"&gt;Slack&lt;/a&gt;. My name is Rob Holland, feel free to reach out to me directly on Slack if you like—would love to hear from you.&lt;/p&gt;

</description>
      <category>temporal</category>
      <category>performance</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
