🌩️ [pt-BR] Ray no k8s: Executando o ray localmente no k3d
Tags: python, kubernetes, ray, k3d
Published: 2023-08-14 23:05:00

Continuando, nessa seção iremos executar uma aplicação extremamente básica. Escrevemos um código que calcula o quadrado de números de 1 até 10, antes de computar o quadrado pausamos a execução por alguns segundos para simular um processo demorado. 

### 2.1 Código sem paralelismo

Abaixo o código que usamos, colocamos ele em um arquivo com o nome `script.py`.

```python
import time


def square(x):
    time.sleep(x * 0.5)
    return {
        "numero": x,
        "quadrado": x * x
    }

start = time.time()
print([square(x+1) for x in range(10)])
print(f"Tarefa executou em {time.time() - start:.2f} segundos")
```
Executamos o código que finalizou em 27.53 segundos, um tempo bem aceitável:
```sh
python script.py
# [{'numero': 1, 'quadrado': 1}, {'numero': 2, 'quadrado': 4}, {'numero': 3, 'quadrado': 9}, {'numero': 4, 'quadrado': 16}, {'numero': 5, 'quadrado': 25}, {'numero': 6, 'quadrado': 36}, {'numero': 7, 'quadrado': 49}, {'numero': 8, 'quadrado': 64}, {'numero': 9, 'quadrado': 81}, {'numero': 10, 'quadrado': 100}]
# Tarefa executou em 27.53 segundos
```

### 2.2 Usando múltiplas CPUs localmente
Colocar esse código para usar os processadores é bem fácil, basta alguns ajustes:

```python
import time
import ray # importar módulo do ray


def square(x):
    time.sleep(x * 0.5)
    return {
        "numero": x,
        "quadrado": x * x
    }

# criar uma task para o ray
@ray.remote
def square_task(x): 
    return square(x)

ray.init() # inicializar o ray
start = time.time()
# print([square(x+1) for x in range(10)]) # antes era assim
print(ray.get([square_task.remote(x+1) for x in range(10)]))
print(f"Tarefa executou em {time.time() - start:.2f} segundos")
```
Fizemos poucas mudanças no código, mas conseguimos diminuir o tempo de execução (tempo poderá ser diferente pois varia de acordo com o equipamento). Abaixo podemos ver que o tempo de execução caiu para 5s, a máquina que executei tem mais de 6 cores.

```sh
python script_ray.py
# 2023-07-30 21:18:46,499 INFO worker.py:1612 -- Started a local Ray instance. View the dashboard at 127.0.0.1:8265
# [{'numero': 1, 'quadrado': 1}, {'numero': 2, 'quadrado': 4}, {'numero': 3, 'quadrado': 9}, {'numero': 4, 'quadrado': 16}, {'numero': 5, 'quadrado': 25}, {'numero': 6, 'quadrado': 36}, {'numero': 7, 'quadrado': 49}, {'numero': 8, 'quadrado': 64}, {'numero': 9, 'quadrado': 81}, {'numero': 10, 'quadrado': 100}]
# Tarefa executou em 5.06 segundos
```
### 2.3 Usando o cluster local do k3d

Primeiro prosseguimos com a instalação do [KubeRay operator](https://ray-project.github.io/kuberay/deploy/helm-cluster/).

```sh
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm install kuberay-operator kuberay/kuberay-operator --version 0.6.0
```
Para confirmar se o operador está em execução, verificamos se o pod está pronto.

```sh
kubectl get pods | grep kuberay-operator
# kuberay-operator-54f657c8cf-p2lh8           1/1     Running   2 (48s ago)   20h
```


Em seguida, criamos um cluster do ray copiando o yaml [exemplo da documentação](https://docs.ray.io/en/releases-2.6.1/cluster/running-applications/job-submission/index.html) fazendo pequenas mudanças. Colocamos 5 pods e menos recursos de memória e cpu, além de atribuirmos o head e os workers a nós específicos.

```yaml
apiVersion: ray.io/v1alpha1
kind: RayCluster
metadata:
  labels:
    controller-tools.k8s.io: "1.0"
  name: raycluster-teste
spec:
  rayVersion: '2.6.1'
  headGroupSpec:
    serviceType: ClusterIP
    rayStartParams:
      dashboard-host: '0.0.0.0'
      block: 'true'
    template:
      spec:
        nodeSelector:
          type: node1 # definimos o head no nó 1
        containers:
        - name: ray-head
          image: rayproject/ray:2.6.1
          imagePullPolicy: Always
          resources: # mudamos para usar menos recursos
            limits: 
              cpu: "1"
              memory: "1Gi"
              ephemeral-storage: "1Gi"
            requests:
              cpu: "1"
              memory: "1Gi"
              ephemeral-storage: "1Gi"
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh","-c","ray stop"]
  workerGroupSpecs: # definimos para criar 5 pods
  - replicas: 5
    minReplicas: 5
    maxReplicas: 5
    rayStartParams:
      block: 'true'
    template:
      spec:
        nodeSelector: 
          type: node2 # workers no nó 2
        containers:
        - name: ray-worker
          image: rayproject/ray:2.6.1
          resources: # mudamos para usar menos recursos
            limits:
              cpu: "1"
              memory: "1Gi"
              ephemeral-storage: "1Gi"
            requests:
              cpu: "1"
              memory: "1Gi"
              ephemeral-storage: "1Gi"
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh","-c","ray stop"]
        - name: init-myservice
          image: busybox:1.28
          command: ['sh', '-c', "until nslookup $RAY_IP.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]

```
Após aplicar o comando `kubectl apply -f ray-cluster.yaml`, verificamos se todos os pods subiram com sucesso. Abaixo vemos os pods do ray em execução.
```sh
kubectl get pods
# NAME                                        READY   STATUS    RESTARTS   AGE
# kuberay-operator-54f657c8cf-52gsk           1/1     Running   0          7m46s
# raycluster-teste-head-mkzr5                 1/1     Running   0          4m6s
# raycluster-teste-worker-large-group-gns8w   1/1     Running   0          4m6s
# raycluster-teste-worker-large-group-cf2br   1/1     Running   0          4m6s
# raycluster-teste-worker-large-group-kl5ln   1/1     Running   0          4m6s
# raycluster-teste-worker-large-group-sslmb   1/1     Running   0          4m6s
# raycluster-teste-worker-large-group-ht64t   1/1     Running   0          4m6s
```
Conforme recomendação da [documentação do k3d](https://k3d.io/v5.5.1/usage/exposing_services/), expomos a aplicação do ray ao criar um Ingress.

```sh
# verificar nome do serviço do ray
kubectl get services | grep raycluster
# raycluster-teste-head-svc   ClusterIP   10.43.99.42    <none>        10001/TCP,8265/TCP,8080/TCP,6379/TCP,8000/TCP   25m
```

```sh
# criar ingress
kubectl apply -f - << END
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mycluster-ingress
  annotations:
    ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: raycluster-teste-head-svc # serviço do ray
            port:
              number: 8265 # porta do serviço do ray
END
```

Por fim, vamos conectar nossa aplicação ao cluster. Na documentação do ray é recomendado utilizar [Ray Jobs](https://docs.ray.io/en/latest/cluster/running-applications/job-submission/index.html) para executar uma aplicação em um cluster. Assim, utilizamos o seguinte comando no mesmo diretório do `script.py`, não precisamos alterar o código em si:

```sh
ray job submit --address http://localhost:8265 --working-dir . -- python script.py
``` 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cojs9hz12zqg5dqvl6bj.png)

Podemos notar que com 5 workers, conseguimos processar a aplicação em 7 segundos. Além disso, vimos que para executar o mesmo código local no cluster basta usar os `Ray Jobs`.

## Limpeza
### Deletar cluster do ray
Para apagar o cluster que criamos nesse exemplo, use o comando abaixo:
```sh
kubectl delete -f ray-cluster.yaml
# Pode levar um tempo para deletar, verifique com o `kubectl get pods`
```

### Deletar o _operator_ do _kubernetes_
Normalmente deixaríamos o operador executando para poder subir outros clusters do _ray_, mas para deletar o recurso usamos o comando abaixo:

```sh
# desinstalar o operator
helm uninstall kuberay-operator
# deletar o ingress que nomeamos como `mycluster-ingress`
kubectl delete ingress mycluster-ingress
```

### Deletar o cluster local do k3d
Por fim, se não desejarmos mais fazer experimentos locais no kubernetes podemos deletar nosso cluster do k3d:
```sh
k3d cluster delete --config k3d-config.yml
```

Esse tutorial foi parte do meu estudo explorando as documentações do Ray e do k3d.