← Back to Index

[!WARNING] Working in progress

RedHat Keycloak-26 Performance Test

We want to test the performance of Red Hat Keycloak 26 on a specific requirements and configurations:

  1. 45 nodes
  2. 50k users in total
  3. 100 db connections per node

install keycloak operator

Create a namespace for Keycloak:

oc new-project demo-keycloak

And install the Keycloak Operator from web UI.

build customer keycloak image

In keycloak configuration options, there are build options and configuration. For the build options, we can only apply such options during container image creation.

Here is the offical documentation, tell us how to create a customized and optimized container image.

To create a customized keycloak image, we can run the keycloak with --optimize option, which will skip the first build stage, and run the keycloak app directly, which will boost the startup times.

for job_id in 0...9; do
          oc delete -n demo-keycloak -f ${BASE_DIR}/data/install/keycloak-script-create-users-${job_id}.yaml
        done
        
        mkdir -p ${BASE_DIR}/data/base-images/keycloak
        cd ${BASE_DIR}/data/base-images/keycloak
        
        cat << EOF > ${BASE_DIR}/data/base-images/keycloak/Dockerfile
        FROM registry.redhat.io/rhbk/keycloak-rhel9:26.0-11 as builder
        
        # Enable health and metrics support
        
        ENV KC_HEALTH_ENABLED=true
        ENV KC_METRICS_ENABLED=true
        
        # Enable tracing support
        
        # ENV KC_TRACING_ENABLED=true
        
        # Enable features for tracing/opentelementry
        
        ENV KC_FEATURES=opentelemetry
        
        # Configure a database vendor
        
        ENV KC_DB=postgres
        
        WORKDIR /opt/keycloak
        
        # for demonstration purposes only, please make sure to use proper certificates in production instead
        
        # RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
        
        RUN /opt/keycloak/bin/kc.sh build
        
        FROM registry.redhat.io/rhbk/keycloak-rhel9:26.0-11
        COPY --from=builder /opt/keycloak/ /opt/keycloak/
        
        # change these values to point to a running postgres instance
        
        ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
        EOF
        
        podman build . -t quay.io/wangzheng422/qimgs:keycloak-26-2025.03.25-v01
        podman push quay.io/wangzheng422/qimgs:keycloak-26-2025.03.25-v01

create a keycloak instance with basic settings

To create a Keycloak instance with basic settings, first you need to create a backend DB. Then, you can create the Keycloak instance based on this DB.

mkdir -p ${BASE_DIR}/data/install/
        
        oc delete -f ${BASE_DIR}/data/install/keycloak-db-pvc.yaml -n demo-keycloak
        
        cat << EOF > ${BASE_DIR}/data/install/keycloak-db-pvc.yaml
        apiVersion: v1
        kind: PersistentVolumeClaim
        metadata:
          name: postgresql-db-pvc
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 100Gi
        EOF
        
        oc create -f ${BASE_DIR}/data/install/keycloak-db-pvc.yaml -n demo-keycloak
        
        
        oc delete -f ${BASE_DIR}/data/install/keycloak-db.yaml -n demo-keycloak
        
        cat << EOF > ${BASE_DIR}/data/install/keycloak-db.yaml
        ---
        apiVersion: apps/v1
        kind: StatefulSet
        metadata:
          name: postgresql-db
        spec:
          serviceName: postgresql-db-service
          selector:
            matchLabels:
              app: postgresql-db
          replicas: 1
          template:
            metadata:
              labels:
                app: postgresql-db
            spec:
              containers:
                - name: postgresql-db
                  image: postgres:15
                  args: ["-c", "max_connections=1000"]
                  volumeMounts:
                    - mountPath: /data
                      name: cache-volume
                  env:
                    - name: POSTGRES_USER
                      value: testuser
                    - name: POSTGRES_PASSWORD
                      value: testpassword
                    - name: PGDATA
                      value: /data/pgdata
                    - name: POSTGRES_DB
                      value: keycloak
              volumes:
                - name: cache-volume
                  persistentVolumeClaim:
                    claimName: postgresql-db-pvc
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: postgres-db
        spec:
          selector:
            app: postgresql-db
          type: LoadBalancer
          ports:
          - port: 5432
            targetPort: 5432
        
        EOF
        
        oc create -f ${BASE_DIR}/data/install/keycloak-db.yaml -n demo-keycloak

Now, we have a Keycloak database running in our OpenShift cluster. Next, we need to configure Keycloak to use this database.


        # create secret needed by keycloak
        
        # the host name here, we use '*' to limit the length of the hostname in the certificate
        
        RHSSO_HOST="*.apps.cluster-b2cpj.b2cpj.sandbox75.opentlc.com"
        
        cd ${BASE_DIR}/data/install/
        
        openssl req -subj "/CN=$RHSSO_HOST/O=Test Keycloak./C=US" -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem
        
        oc delete secret example-tls-secret -n demo-keycloak
        oc create secret tls example-tls-secret --cert certificate.pem --key key.pem -n demo-keycloak
        
        
        oc delete secret keycloak-db-secret -n demo-keycloak
        oc create secret generic keycloak-db-secret -n demo-keycloak \
          --from-literal=username=testuser \
          --from-literal=password=testpassword
        
        
        # here we create keycloak instance with postgres db and tls secret
        
        # here we change back the host name to actual hostname
        
        oc delete -f ${BASE_DIR}/data/install/keycloak.yaml -n demo-keycloak
        
        # RHSSO_HOST="keycloak-demo-keycloak.apps.demo-01-rhsys.wzhlab.top"
        
        RHSSO_HOST="example-kc-demo-keycloak.apps.cluster-b2cpj.b2cpj.sandbox75.opentlc.com"
        
        RHSSO_IMAGE="quay.io/wangzheng422/qimgs:keycloak-26-2025.03.25-v01"
        
        cat << EOF > ${BASE_DIR}/data/install/keycloak.yaml
        apiVersion: k8s.keycloak.org/v2alpha1
        kind: Keycloak
        metadata:
          name: example-kc
        spec:
          instances: 1
          db:
            vendor: postgres
            host: postgres-db
            usernameSecret:
              name: keycloak-db-secret
              key: username
            passwordSecret:
              name: keycloak-db-secret
              key: password
          http:
            tlsSecret: example-tls-secret
            httpEnabled: true
          # ingress:
          #   className: openshift-default
          hostname:
            hostname: $RHSSO_HOST
          proxy:
            headers: xforwarded
          image: $RHSSO_IMAGE
          startOptimized: true
        EOF
        
        oc create -f ${BASE_DIR}/data/install/keycloak.yaml -n demo-keycloak
        
        # get the keycloak initial admin user and password
        
        oc get secret example-kc-initial-admin -n demo-keycloak -o jsonpath='{.data.username}' | base64 --decode && echo
        
        # temp-admin
        
        oc get secret example-kc-initial-admin -n demo-keycloak -o jsonpath='{.data.password}' | base64 --decode && echo
        
        # 0xxxxxxxxxxxxxxxxxxxxxxxxe
        
        
        # in postgresql pod terminal
        
        # we can see the current value of max_connections is 1000. We can change it to a higher value if needed.
        
        psql -U testuser -d keycloak
        
        # Type "help" for help.
        
        # keycloak=# SHOW max_connections;
        
        #  max_connections 
        
        # -----------------
        
        #  1000
        
        # (1 row)

check the keycloak instance on web interface

Goto https://example-kc-demo-keycloak.apps.cluster-r9m7r.r9m7r.sandbox2453.opentlc.com and login with admin credentials. Then you can see the rhbk default admin console.

For testing purposes, we will extend the session timeout to several days.

[!WARNING] For testing only, do not use this configuration in production.

get current keycloak config

Let us check the config of keycloak instance from web console. Current version is 26.0.10-opr.1.

apiVersion: k8s.keycloak.org/v2alpha1
        kind: Keycloak
        metadata:
          name: example-kc
          namespace: demo-keycloak
        spec:
          db:
            host: postgres-db
            passwordSecret:
              key: password
              name: keycloak-db-secret
            usernameSecret:
              key: username
              name: keycloak-db-secret
            vendor: postgres
          hostname:
            hostname: example-kc-demo-keycloak.apps.cluster-b2cpj.b2cpj.sandbox75.opentlc.com
          http:
            httpEnabled: true
            tlsSecret: example-tls-secret
          # image: 'quay.io/wangzheng422/qimgs:keycloak-26-2025.03.25-v01'
          instances: 1
          proxy:
            headers: xforwarded
          # startOptimized: true

Next, we will check the contents of the /opt/keycloak/conf directory inside the example-kc-0 pod in the demo-keycloak namespace. This will help us learn more about how the Keycloak server is configured.


        oc exec -it example-kc-0 -n demo-keycloak -- ls /opt/keycloak/conf
        
        # cache-ispn.xml  keycloak.conf  README.md  truststores
        
        # oc exec -it example-kc-0 -n demo-keycloak -- ls -R /opt/keycloak
        
        oc exec -it example-kc-0 -n demo-keycloak -- cat /opt/keycloak/conf/keycloak.conf

content of /opt/keycloak/conf/keycloak.conf


        # Basic settings for running in production. Change accordingly before deploying the server.

        # Database

        # The database vendor.

        #db=postgres

        # The username of the database user.

        #db-username=keycloak

        # The password of the database user.

        #db-password=password

        # The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor.

        #db-url=jdbc:postgresql://localhost/keycloak

        # Observability

        # If the server should expose healthcheck endpoints.

        #health-enabled=true

        # If the server should expose metrics endpoints.

        #metrics-enabled=true

        # HTTP

        # The file path to a server certificate or certificate chain in PEM format.

        #https-certificate-file=${kc.home.dir}conf/server.crt.pem

        # The file path to a private key in PEM format.

        #https-certificate-key-file=${kc.home.dir}conf/server.key.pem

        # The proxy address forwarding mode if the server is behind a reverse proxy.

        #proxy=reencrypt

        # Do not attach route to cookies and rely on the session affinity capabilities from reverse proxy

        #spi-sticky-session-encoder-infinispan-should-attach-route=false

        # Hostname for the Keycloak server.

        #hostname=myhostname

Next, let’s check the cache-ispn.xml file. We can do this by running the following command:

oc exec -it example-kc-0 -n demo-keycloak -- cat /opt/keycloak/conf/cache-ispn.xml

And the content of cache-ispn.xml is:

<?xml version="1.0" encoding="UTF-8"?>
        <!--
          ~ Copyright 2019 Red Hat, Inc. and/or its affiliates
          ~ and other contributors as indicated by the @author tags.
          ~
          ~ Licensed under the Apache License, Version 2.0 (the "License");
          ~ you may not use this file except in compliance with the License.
          ~ You may obtain a copy of the License at
          ~
          ~ http://www.apache.org/licenses/LICENSE-2.0
          ~
          ~ Unless required by applicable law or agreed to in writing, software
          ~ distributed under the License is distributed on an "AS IS" BASIS,
          ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
          ~ See the License for the specific language governing permissions and
          ~ limitations under the License.
          -->
        
        <infinispan
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd"
                xmlns="urn:infinispan:config:15.0">
        
            <cache-container name="keycloak">
                <transport lock-timeout="60000" stack="udp"/>
                <local-cache name="realms" simple-cache="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <memory max-count="10000"/>
                </local-cache>
                <local-cache name="users" simple-cache="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <memory max-count="10000"/>
                </local-cache>
                <distributed-cache name="sessions" owners="1">
                    <expiration lifespan="-1"/>
                    <memory max-count="10000"/>
                </distributed-cache>
                <distributed-cache name="authenticationSessions" owners="2">
                    <expiration lifespan="-1"/>
                </distributed-cache>
                <distributed-cache name="offlineSessions" owners="1">
                    <expiration lifespan="-1"/>
                    <memory max-count="10000"/>
                </distributed-cache>
                <distributed-cache name="clientSessions" owners="1">
                    <expiration lifespan="-1"/>
                    <memory max-count="10000"/>
                </distributed-cache>
                <distributed-cache name="offlineClientSessions" owners="1">
                    <expiration lifespan="-1"/>
                    <memory max-count="10000"/>
                </distributed-cache>
                <distributed-cache name="loginFailures" owners="2">
                    <expiration lifespan="-1"/>
                </distributed-cache>
                <local-cache name="authorization" simple-cache="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <memory max-count="10000"/>
                </local-cache>
                <replicated-cache name="work">
                    <expiration lifespan="-1"/>
                </replicated-cache>
                <local-cache name="keys" simple-cache="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <expiration max-idle="3600000"/>
                    <memory max-count="1000"/>
                </local-cache>
                <distributed-cache name="actionTokens" owners="2">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <expiration max-idle="-1" lifespan="-1" interval="300000"/>
                    <memory max-count="-1"/>
                </distributed-cache>
            </cache-container>
        </infinispan>

It is interesting to see how the cache-ispn.xml file has been modified since rhbk-24, espeically in the sessions section and the <memory max-count="10000"/> configuration.

monitor the keycloak instance

Now, we need to configure openshift-monitoring to monitor the keycloak instance. We can do this by creating a configmap and applying it to the cluster. Here is an example of how to create the configmap and monitoring settings.


        # create a configmap for openshift-monitoring to enable monitoring for customer workload
        
        cat << EOF > ${BASE_DIR}/data/install/enable-monitor.yaml
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: cluster-monitoring-config
          namespace: openshift-monitoring
        data:
          config.yaml: |
            enableUserWorkload: true 
            # alertmanagerMain:
            #   enableUserAlertmanagerConfig: true 
        EOF
        
        oc apply -f ${BASE_DIR}/data/install/enable-monitor.yaml
        
        oc -n openshift-user-workload-monitoring get pod
        
        # monitor keycloak
        
        oc delete -n demo-keycloak -f ${BASE_DIR}/data/install/keycloak-monitor.yaml
        
        cat << EOF > ${BASE_DIR}/data/install/keycloak-monitor.yaml
        ---
        apiVersion: monitoring.coreos.com/v1
        kind: ServiceMonitor
        metadata:
          name: keycloak
          namespace: demo-keycloak
        spec:
          endpoints:
            - interval: 5s
              path: /metrics
              port: management
              scheme: https
              tlsConfig:
                insecureSkipVerify: true
          namespaceSelector:
            matchNames:
              - demo-keycloak
          selector:
            matchLabels:
              app: keycloak
        EOF
        
        oc apply -f ${BASE_DIR}/data/install/keycloak-monitor.yaml -n demo-keycloak

check the metric on ocp web console

By login to the ocp web console, we can check the metric on the dashboard, using jboss_infinispan_number_of_entries_in_memory as metrics, we can see internal statics of keycloak/infinispan.

create testing data in keycloak

We will create some (50k) users under a new realm in Keycloak to test the performance of keycloak.

create keycloak-tool container image

We need a pod to run testing scripts, and the script will call keycloak API to perform operations. So we will create a dedicated container image for the pod.

We will use redhat official container image for keycloak. You can find the image on Red Hat Container Catalog:


        # as root
        
        mkdir -p ${BASE_DIR}/data/keycloak.tool
        cd ${BASE_DIR}/data/keycloak.tool
        
        cat << 'EOF' > bashrc
        alias ls='ls --color=auto'
        export PATH=/opt/keycloak/bin:$PATH
        EOF
        
        
        cat << EOF > Dockerfile
        FROM registry.redhat.io/ubi9/ubi AS ubi-micro-build
        RUN mkdir -p /mnt/rootfs
        RUN dnf install --installroot /mnt/rootfs  --releasever 9 --setopt install_weak_deps=false --nodocs -y /usr/bin/ps bash-completion coreutils /usr/bin/curl jq python3 /usr/bin/tar /usr/bin/sha256sum vim nano && \
            dnf --installroot /mnt/rootfs clean all && \
            rpm --root /mnt/rootfs -e --nodeps setup
        
        FROM registry.redhat.io/rhbk/keycloak-rhel9:26.0-11
        COPY --from=ubi-micro-build /mnt/rootfs /
        COPY bashrc /opt/keycloak/.bashrc
        EOF
        
        podman build -t quay.io/wangzheng422/qimgs:keycloak-26.tool-2025-03-21-v01 .
        
        podman push quay.io/wangzheng422/qimgs:keycloak-26.tool-2025-03-21-v01
        
        podman run -it --entrypoint /bin/bash quay.io/wangzheng422/qimgs:keycloak-26.tool-2025-03-21-v01

deploy keycloak tool on ocp


        oc delete -n demo-keycloak -f ${BASE_DIR}/data/install/keycloak.tool.yaml
        
        cat << EOF > ${BASE_DIR}/data/install/keycloak.tool.yaml
        apiVersion: v1
        kind: Pod
        metadata:
          name: keycloak-tool
        spec:
          containers:
          - name: keycloak-tool-container
            image: quay.io/wangzheng422/qimgs:keycloak-26.tool-2025-03-21-v01
            command: ["tail", "-f", "/dev/null"]
        EOF
        
        oc apply -f ${BASE_DIR}/data/install/keycloak.tool.yaml -n demo-keycloak
        
        # start the shell
        
        oc exec -it keycloak-tool -n demo-keycloak -- bash
        
        # copy something out
        
        oc cp -n demo-keycloak keycloak-tool:/opt/keycloak/metrics ./metrics

init demo users

First, we need to create 50k user in keycloak.

Lets do it by using keycloak admin cli.

This is the script framework, and we’ve written the entire process here. However, we are only creating realms. The script for creating users only demonstrates the creation logic; there are dedicated chapters later that will use more techniques to create users.


        ADMIN_PWD='0xxxxxxxxxxxxxxxxxxxxxxxxxxe'
        
        # after enable http in keycloak, you can use http endpoint
        
        # it is better to set session timeout for admin for 1 day :)
        
        kcadm.sh config credentials --server http://example-kc-service:8080/ --realm master --user temp-admin --password $ADMIN_PWD
        
        # create a realm
        
        kcadm.sh create realms -s realm=performance -s enabled=true
        
        # Set SSO Session Max and SSO Session Idle to 1 day (1440 minutes)
        
        kcadm.sh update realms/performance -s 'ssoSessionMaxLifespan=86400' -s 'ssoSessionIdleTimeout=86400'
        
        # delete the realm
        
        kcadm.sh delete realms/performance
        
        # create a client
        
        kcadm.sh create clients -r performance -s clientId=performance -s enabled=true -s 'directAccessGrantsEnabled=true'
        
        # delete the client
        
        CLIENT_ID=$(kcadm.sh get clients -r performance -q clientId=performance | jq -r '.[0].id')
        if [ -n "$CLIENT_ID" ]; then
          echo "Deleting client performance"
          kcadm.sh delete clients/$CLIENT_ID -r performance
        else
          echo "Client performance not found"
        fi
        
        # create 50k user, from user-00001 to user-50000, and set password for each user
        
        for i in {1..50000}; do
          echo "Creating user user-$(printf "%05d" $i)"
          kcadm.sh create users -r performance -s username=user-$(printf "%05d" $i) -s enabled=true -s email=user-$(printf "%05d" $i)@wzhlab.top -s firstName=First-$(printf "%05d" $i) -s lastName=Last-$(printf "%05d" $i)
          kcadm.sh set-password -r performance --username user-$(printf "%05d" $i) --new-password password
        done
        
        # Delete users
        
        for i in {1..50000}; do
          USER_ID=$(kcadm.sh get users -r performance -q username=user-$(printf "%05d" $i) | jq -r '.[0].id')
          if [ -n "$USER_ID" ]; then
            echo "Deleting user user-$(printf "%05d" $i)"
            kcadm.sh delete users/$USER_ID -r performance
          else
            echo "User user-$(printf "%05d" $i) not found"
          fi
        done

create user using job

Now we try to create users using jobs.

First, we create a service account, and assign priviledge to it, so it can run the script in keycloak-tool.


        oc delete -n demo-keycloak -f ${BASE_DIR}/data/install/keycloak-script-create-users.yaml
        
        cat << EOF > ${BASE_DIR}/data/install/keycloak-script-sa.yaml
        ---
        apiVersion: v1
        kind: ServiceAccount
        metadata:
          name: keycloak-sa
          namespace: demo-keycloak
        ---
        apiVersion: security.openshift.io/v1
        kind: SecurityContextConstraints
        metadata:
          name: keycloak-scc
        allowHostDirVolumePlugin: false
        allowHostIPC: false
        allowHostNetwork: false
        allowHostPID: false
        allowHostPorts: false
        allowPrivilegeEscalation: true
        allowPrivilegedContainer: false
        allowedCapabilities: []
        defaultAddCapabilities: []
        fsGroup:
          type: RunAsAny
        groups: []
        priority: null
        readOnlyRootFilesystem: false
        requiredDropCapabilities: []
        runAsUser:
          type: MustRunAs
          uid: 1000
        seLinuxContext:
          type: RunAsAny
        seccompProfiles:
        
        - '*'
        supplementalGroups:
          type: RunAsAny
        users:
        
        - system:serviceaccount:demo-keycloak:keycloak-sa
        volumes:
        
        - configMap
        - emptyDir
        - projected
        - secret
        - downwardAPI
        EOF
        
        oc apply -f ${BASE_DIR}/data/install/keycloak-script-sa.yaml -n demo-keycloak
        
        oc adm policy add-scc-to-user keycloak-scc -z keycloak-sa -n demo-keycloak

create user use multiple jobs


        TOTAL_USERS=50000
        NUM_JOBS=10
        USERS_PER_JOB=$((TOTAL_USERS / NUM_JOBS))
        
        for job_id in $(seq 1 $NUM_JOBS); do
          START_USER=$(( (job_id - 1) * USERS_PER_JOB + 1 ))
          END_USER=$(( job_id * USERS_PER_JOB ))
        
          cat << EOF > ${BASE_DIR}/data/install/keycloak-script-create-users-${job_id}.yaml
        
        ---
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: keycloak-script-config-${job_id}
        data:
          create-users.sh: |
            kcadm.sh config credentials --server http://example-kc-service:8080/ --realm master --user temp-admin --password $ADMIN_PWD 
        
            for i in {$START_USER..$END_USER}; do
              echo "Creating user user-\$(printf "%05d" \$i)"
              kcadm.sh create users -r performance -s username=user-\$(printf "%05d" \$i) -s enabled=true -s email=user-\$(printf "%05d" \$i)@wzhlab.top -s firstName=First-\$(printf "%05d" \$i) -s lastName=Last-\$(printf "%05d" \$i)
              kcadm.sh set-password -r performance --username user-\$(printf "%05d" \$i) --new-password password
            done
        
        ---
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: keycloak-create-users-job-${job_id}
        spec:
          template:
            spec:
              serviceAccountName: keycloak-sa
              containers:
              - name: keycloak-tool
                image: quay.io/wangzheng422/qimgs:keycloak-26.tool-2025-03-21-v01
                command: ["/bin/bash", "-c"]
                args: ["source /opt/keycloak/.bashrc && cp /scripts/create-users.sh /tmp/create-users.sh && chmod +x /tmp/create-users.sh && bash /tmp/create-users.sh"]
                securityContext:
                  runAsUser: 1000
                volumeMounts:
                - name: script-volume
                  mountPath: /scripts
              restartPolicy: Never
              volumes:
              - name: script-volume
                configMap:
                  name: keycloak-script-config-${job_id}
          backoffLimit: 4
        EOF
          oc delete -n demo-keycloak -f ${BASE_DIR}/data/install/keycloak-script-create-users-${job_id}.yaml
          oc apply -f ${BASE_DIR}/data/install/keycloak-script-create-users-${job_id}.yaml -n demo-keycloak
        done
        
        
        # if you want to remove the jobs
        
        for job_id in {1..10}; do
          oc delete -n demo-keycloak -f ${BASE_DIR}/data/install/keycloak-script-create-users-${job_id}.yaml
        done
        

If everything ok, you can see those users in web console.

test performance using curl

Now, we have 50k user in keycloak. Lets test the performance of keycloak.

Get the client secert from keycloak’s web console.

And test the login rest api using curl.


        # test the performance of keycloak, by login with each user
        
        CLIENT_SECRET="hxxxxxxxxxxxxxxxxxxxxq"
        
        curl -X POST 'http://example-kc-service:8080/realms/performance/protocol/openid-connect/token' \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "client_id=performance" \
        -d "client_secret=$CLIENT_SECRET" \
        -d "username=user-00001" \
        -d "password=password" \
        -d "grant_type=password" | jq .
        
        # {
        
        #   "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJick9pa2tPX3l2dmtoVzlLc05zTEVUMWctSWhfZ0g2WExZZnE5U1ZfeXZFIn0.eyJleHAiOjE3MjgyMjY5NTgsImlhdCI6MTcyODIyNjY1OCwianRpIjoiMzQ5ZGZjZTctNzY1Zi00Yjc0LTgyNjMtMzlmZmQ2NDA3ZjYwIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay1kZW1vLWtleWNsb2FrLmFwcHMuZGVtby0wMS1yaHN5cy53emhsYWIudG9wL3JlYWxtcy9wZXJmb3JtYW5jZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxZWMxMmRhZC0wMWMwLTQ5N2YtOTkzMS0xZjIyMGJiMmI5OTMiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJwZXJmb3JtYW5jZSIsInNlc3Npb25fc3RhdGUiOiIyOWQzYTUyZC0zNjExLTQ4YzktOWM5MC0yOTE2YmMxY2Q2ODciLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtcGVyZm9ybWFuY2UiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMjlkM2E1MmQtMzYxMS00OGM5LTljOTAtMjkxNmJjMWNkNjg3IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiRmlyc3QtMDAwMDEgTGFzdC0wMDAwMSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXItMDAwMDEiLCJnaXZlbl9uYW1lIjoiRmlyc3QtMDAwMDEiLCJmYW1pbHlfbmFtZSI6Ikxhc3QtMDAwMDEiLCJlbWFpbCI6InVzZXItMDAwMDFAd3pobGFiLnRvcCJ9.ioqCjbSuolrhGDPW8SF_Ls0NTOn9mJM8QO7btRo7N24lLZrNaKNrv7R5Mvcs4Bu5xDuB5KHEDh-IU-c3iT8TRK8hc5DHhWYwe7_WICp_O7DQEVIP-9wgeqSY4qmdwBkXvwYN0q8AIOjRwYOYqTP6rLcWiPEhdWDqkCL-S9tyhYBwRt44-k455zi1JOFSBd_vWVXp68TJ5b8TWResz3L-cT02Fk0y9_RZBXang1I3tZUOqpHBCVBhRlDwAvst2QtE3tG-rnIXBR4l1vVn1TXlfoRiDwXE5ski9B1KhHuRNZEqbPdkFpWIfb01h9qwtygv4yNKJEW_knw5t_7iaOwRhA",
        
        #   "expires_in": 300,
        
        #   "refresh_expires_in": 86400,
        
        #   "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2YWY4Mjc5Mi02NmQ3LTQ0OWItODI4MS0wY2M0NWU4ZjU0ZTkifQ.eyJleHAiOjE3MjgzMTMwNTgsImlhdCI6MTcyODIyNjY1OCwianRpIjoiYjczYWYxODktZDQzZi00MjZiLWJhZGYtNjc0NTI3MGIzZWIzIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay1kZW1vLWtleWNsb2FrLmFwcHMuZGVtby0wMS1yaHN5cy53emhsYWIudG9wL3JlYWxtcy9wZXJmb3JtYW5jZSIsImF1ZCI6Imh0dHBzOi8va2V5Y2xvYWstZGVtby1rZXljbG9hay5hcHBzLmRlbW8tMDEtcmhzeXMud3pobGFiLnRvcC9yZWFsbXMvcGVyZm9ybWFuY2UiLCJzdWIiOiIxZWMxMmRhZC0wMWMwLTQ5N2YtOTkzMS0xZjIyMGJiMmI5OTMiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoicGVyZm9ybWFuY2UiLCJzZXNzaW9uX3N0YXRlIjoiMjlkM2E1MmQtMzYxMS00OGM5LTljOTAtMjkxNmJjMWNkNjg3Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMjlkM2E1MmQtMzYxMS00OGM5LTljOTAtMjkxNmJjMWNkNjg3In0.un_vmkLIo8elfXAwrgYAnCd6xMHtPkER1j7xuxaDn_lbdmFJSBYJld4YdB6Rxezv7auOmEdd9y1GiFGd3SOGUw",
        
        #   "token_type": "Bearer",
        
        #   "not-before-policy": 0,
        
        #   "session_state": "29d3a52d-3611-48c9-9c90-2916bc1cd687",
        
        #   "scope": "email profile"
        
        # }

We can see it is ok to call rest api to login, so we can carry out out testing by using the login rest api.

test with python job

We also need a benchmark tool to test the rhsso, we can use python to write a simple script to do this. The python has a build-in prometheus client, so we can monitor the python pod with prometheus.

Make changes to performance_test_keycloak.py, espeically the client secert, then copy file performance_test_keycloak.py from ./files to bastation. And create jobs using the python script.

URL='http://example-kc-service:8080/realms/performance/protocol/openid-connect/token'
        
        VAR_PROJECT=demo-keycloak
        
        # copy file performance_test_keycloak.py from ./files
        
        oc delete -n demo-keycloak configmap performance-test-script
        oc create configmap performance-test-script -n demo-keycloak --from-file=${BASE_DIR}/data/install/performance_test_keycloak.py
        
        
        oc delete -n $VAR_PROJECT -f ${BASE_DIR}/data/install/performance-test-deployment.yaml
        
        cat << EOF > ${BASE_DIR}/data/install/performance-test-deployment.yaml
        ---
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: performance-test-deployment
        spec:
          replicas: 1
          selector:
            matchLabels:
              app: performance-test
          template:
            metadata:
              labels:
                app: performance-test
            spec:
              containers:
              - name: performance-test
                image: quay.io/wangzheng422/qimgs:rocky9-test-2024.10.14.v01 
                command: ["/usr/bin/python3", "/scripts/performance_test_keycloak.py"]
                env:
                - name: CLIENT_SECRET
                  value: $CLIENT_SECRET  # Replace this with your actual client key here.
                - name: NUM_USERS
                  value: "50000"
                - name: NUM_THREADS
                  value: "100"
                volumeMounts:
                - name: script-volume
                  mountPath: /scripts
              restartPolicy: Always
              volumes:
              - name: script-volume
                configMap:
                  name: performance-test-script
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: performance-test-service
          labels:
            app: performance-test
        spec:
          selector:
            app: performance-test
          ports:
            - name: http
              protocol: TCP
              port: 8000
              targetPort: 8000
        EOF
        
        
        oc apply -f ${BASE_DIR}/data/install/performance-test-deployment.yaml -n $VAR_PROJECT

monitor metrics using ocp monitoring

Our performance testing script has promethus build-in, so we can monitor the performance test result using openshift monitoring subsystem.

VAR_PROJECT=demo-keycloak
        
        oc delete -n $VAR_PROJECT -f ${BASE_DIR}/data/install/performance-monitor.yaml
        
        cat << EOF > ${BASE_DIR}/data/install/performance-monitor.yaml
        ---
        apiVersion: monitoring.coreos.com/v1
        kind: ServiceMonitor
        metadata:
          name: performance-test
          namespace: $VAR_PROJECT
        spec:
          endpoints:
            - interval: 5s
              path: /metrics
              port: http
              scheme: http
          namespaceSelector:
            matchNames:
              - $VAR_PROJECT
          selector:
            matchLabels:
              app: performance-test
        EOF
        
        oc apply -f ${BASE_DIR}/data/install/performance-monitor.yaml -n $VAR_PROJECT

You can checkout the metrics by search the name begins with wzh, like wzh_avg_time_sec and wzh_success_rate_sec

patch to the keycloak instance config using 2 owner 2 instance.

Update cache-ispn.xml with the following content, I update the memory max-count to 50kx100 = 5000k = 5m, which is 10 times the target user number, and update lifespan to 1 day, which is based on use case, also enable statistics, and set owners to 2.


        cat << EOF >  ${BASE_DIR}/data/install/keycloak.cache-ispn.xml
        <infinispan
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd"
                xmlns="urn:infinispan:config:15.0">
        
            <cache-container name="keycloak" statistics="true">
                <transport lock-timeout="60000" stack="udp"/>
                <metrics names-as-tags="true" />
                <local-cache name="realms" simple-cache="true" statistics="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <memory max-count="10000"/>
                </local-cache>
                <local-cache name="users" simple-cache="true" statistics="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <memory max-count="5000000"/>
                </local-cache>
                <distributed-cache name="sessions" owners="2" statistics="true">
                    <expiration lifespan="86400"/>
                </distributed-cache>
                <distributed-cache name="authenticationSessions" owners="2" statistics="true">
                    <expiration lifespan="86400"/>
                    <memory max-count="5000000"/>
                </distributed-cache>
                <distributed-cache name="offlineSessions" owners="2" statistics="true">
                    <expiration lifespan="86400"/>
                    <memory max-count="500000"/>
                </distributed-cache>
                <distributed-cache name="clientSessions" owners="2" statistics="true">
                    <expiration lifespan="86400"/>
                    <memory max-count="500000"/>
                </distributed-cache>
                <distributed-cache name="offlineClientSessions" owners="2" statistics="true">
                    <expiration lifespan="86400"/>
                    <memory max-count="5000000"/>
                </distributed-cache>
                <distributed-cache name="loginFailures" owners="2" statistics="true">
                    <expiration lifespan="86400"/>
                    <memory max-count="5000000"/>
                </distributed-cache>
                <local-cache name="authorization" simple-cache="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <memory max-count="5000000"/>
                </local-cache>
                <replicated-cache name="work" statistics="true">
                    <expiration lifespan="86400"/>
                </replicated-cache>
                <local-cache name="keys" simple-cache="true" statistics="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <expiration max-idle="3600000"/>
                    <memory max-count="5000000"/>
                </local-cache>
                <distributed-cache name="actionTokens" owners="2" statistics="true">
                    <encoding>
                        <key media-type="application/x-java-object"/>
                        <value media-type="application/x-java-object"/>
                    </encoding>
                    <expiration max-idle="86400" lifespan="86400" interval="300000"/>
                    <memory max-count="5000000"/>
                </distributed-cache>
            </cache-container>
        </infinispan>
        EOF
        
        # create configmap
        
        oc delete configmap keycloak-cache-ispn -n demo-keycloak
        oc create configmap keycloak-cache-ispn --from-file=${BASE_DIR}/data/install/keycloak.cache-ispn.xml -n demo-keycloak

Then, we update the config of the keycloak instance to enable HTTP and configure the cache, enable metrics.

apiVersion: k8s.keycloak.org/v2alpha1
        kind: Keycloak
        metadata:
          name: example-kc
          namespace: demo-keycloak
        spec:
          ......
          http:
            httpEnabled: true
          cache:
            configMapFile:
              key: keycloak.cache-ispn.xml
              name: keycloak-cache-ispn
          db:
            poolMaxSize: 100
          resources:
            requests:
              memory: "2Gi"
            limits:
              memory: "64Gi"
          additionalOptions:
            - name: metrics-enabled
              value: 'true'
            # - name: cache-metrics-histograms-enabled
            #   value: 'true'
            - name: features-disabled
              value: 'persistent-user-sessions'
            - name: log-console-level
              value: 'error'
            # - name: log-level
            #   value: debug
          instances: 2
          ......

It seems the config is enabled using env. Here is part of the keycload pod yaml file:

kind: Pod
        apiVersion: v1
        metadata:
          name: example-kc-0
          namespace: demo-keycloak
          ......
        spec:
          ......
          containers:
            - name: keycloak
              ......
              env:
                - name: KC_HOSTNAME
                  value: example-kc-demo-keycloak.apps.cluster-r9m7r.r9m7r.sandbox2453.opentlc.com
                - name: KC_HTTP_ENABLED
                  value: 'true'
                - name: KC_HTTP_PORT
                  value: '8080'
                - name: KC_HTTPS_PORT
                  value: '8443'
                - name: KC_HTTPS_CERTIFICATE_FILE
                  value: /mnt/certificates/tls.crt
                - name: KC_HTTPS_CERTIFICATE_KEY_FILE
                  value: /mnt/certificates/tls.key
                - name: KC_DB
                  value: postgres
                - name: KC_DB_USERNAME
                  valueFrom:
                    secretKeyRef:
                      name: keycloak-db-secret
                      key: username
                - name: KC_DB_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: keycloak-db-secret
                      key: password
                - name: KC_DB_URL_HOST
                  value: postgres-db
                - name: KC_DB_POOL_MAX_SIZE
                  value: '100'
                - name: KC_CACHE_CONFIG_FILE
                  value: cache/keycloak.cache-ispn.xml
                - name: KC_PROXY_HEADERS
                  value: xforwarded
                - name: KC_BOOTSTRAP_ADMIN_USERNAME
                  valueFrom:
                    secretKeyRef:
                      name: example-kc-initial-admin
                      key: username
                - name: KC_BOOTSTRAP_ADMIN_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: example-kc-initial-admin
                      key: password
                - name: KC_HEALTH_ENABLED
                  value: 'true'
                - name: KC_CACHE
                  value: ispn
                - name: KC_CACHE_STACK
                  value: kubernetes
                - name: KC_TRUSTSTORE_PATHS
                  value: '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt,/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt'
                - name: KC_TRACING_SERVICE_NAME
                  value: example-kc
                - name: KC_TRACING_RESOURCE_ATTRIBUTES
                  value: k8s.namespace.name=demo-keycloak
          ......

monitoring and reporting

We will use 2 keycloak instances, and 2 owner as baseline. We will use 100 testing pod, which is 100x10=1000 simulated users.

keycloak’s cpu and memory usage looks like cpu: 24cpu, and memory: 16GB (with session persistence enabled)

And, it seems there is a increase for http request times, and the reason maybe

If without user session storage in database, it will increase to 30cpu, 50GB

And it seems no increase for http request time.

Another interseting thing is that the number of logins/second, without user session persistence, seems to be much faster than with user session persistence, from ~400 to ~1100 logins/second.

The keycloak response time is relative stable at 200ms, and success rate is 100%.

conclusion

Based on the performance test results, the Keycloak system demonstrates stable response times and a 100% success rate. These results suggest that the Keycloak system is effectively handling requests and maintaining high performance levels. Additional testing and optimization may be required to ensure consistent performance across different use cases.

end