Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

pyconjp2021-locust

Tatch
October 16, 2021

 pyconjp2021-locust

Tatch

October 16, 2021
Tweet

More Decks by Tatch

Other Decks in Programming

Transcript

  1. -PDVTUͱ͸ʁ8IBU`T-PDVTU 1ZUIPO੡ͷෛՙࢼݧϑϨʔϜϫʔΫ w ๛෋ͳαϯϓϧίʔυ΍ɺศརͳϓϥάΠϯ΋͋Δ 
 -PUTPGFYBNQMFTBOEVTFGVMQMVHJOTBSFBWBJMBCMF 
 IUUQTHJUIVCDPNMPDVTUJPMPDVTUUSFFNBTUFSFYBNQMFT 
 IUUQTHJUIVCDPN4WFOTLB4QFMMPDVTUQMVHJOT

    
 w ࡢ೥ͷ1Z$PO+1ʹͱͯ΋ྑ͍ൃද͕͋ΔͷͰੋඇΈͯ΄͍͠Ͱ͢ 
 "OPUIFSHSFBUUBMLBCPVU-PDVTUJO1Z$PO+1 w ʮ1ZUIPOͰ࢝ΊΔෛՙࢼݧʯIUUQTQZDPOKQFOUJNFUBCMF JE -PBEUFTUJOHGSBNFXPSLXSJUUFOJO1ZUIPO
  2. ෛՙࢼݧͷྲྀΕ4UFQTUPSVOMPBEUFTUJOH ໨ඪઃఆ 
 Set objective ςετઃܭ 
 Write scenario ςετ࣮ࢪ

    Run tests ݁Ռ෼ੳ Check results ໨ඪୡ੒ʂ 
 Done! νϡʔχϯά 
 Tune your app
  3. ෛՙࢼݧͷྲྀΕ4UFQTUPSVOMPBEUFTUJOH ໨ඪઃఆ 
 Set objective ςετઃܭ 
 Write scenario ςετ࣮ࢪ

    Run tests ݁Ռ෼ੳ Check results ໨ඪୡ੒ʂ 
 Done! νϡʔχϯά 
 Tune your app
  4. ෛՙࢼݧͷྲྀΕ4UFQTUPSVOMPBEUFTUJOH ໨ඪઃఆ 
 Set objective ςετઃܭ 
 Write scenario ςετ࣮ࢪ

    Run tests ݁Ռ෼ੳ Check results ໨ඪୡ੒ʂ 
 Done! νϡʔχϯά 
 Tune your app locustfile.pyΛॻ͘ 
 Write locustfile.py
  5. ෛՙࢼݧͷྲྀΕ4UFQTUPSVOMPBEUFTUJOH ໨ඪઃఆ 
 Set objective ςετઃܭ 
 Write scenario ςετ࣮ࢪ

    Run tests ݁Ռ෼ੳ Check results ໨ඪୡ੒ʂ 
 Done! νϡʔχϯά 
 Tune your app Run locust
  6. ෛՙࢼݧͷྲྀΕ4UFQTUPSVOMPBEUFTUJOH ໨ඪઃఆ 
 Set objective ςετઃܭ 
 Write scenario ςετ࣮ࢪ

    Run tests ݁Ռ෼ੳ Check results ໨ඪୡ੒ʂ 
 Done! νϡʔχϯά 
 Tune your app ݁ՌΛݟͯϘτϧωοΫΛݟ͚ͭΔ 
 Check results and find bottlenecks
  7. ෛՙࢼݧͷྲྀΕ4UFQTUPSVOMPBEUFTUJOH ໨ඪઃఆ 
 Set objective ςετઃܭ 
 Write scenario ςετ࣮ࢪ

    Run tests ݁Ռ෼ੳ Check results ໨ඪୡ੒ʂ 
 Done! νϡʔχϯά 
 Tune your app
  8. Ϩεϙϯεͷৄࡉͳ֬ೝ7BMJEBUFSFTQPOTFTJOEFUBJM @app.post("/auth") def auth(user: User, resp: Response): req_body = user.dict()

    # {"name": "alice1234", "password": "alice1234"} if user_collection.find_one(req_body): resp.status_code = status.HTTP_200_OK resp.set_cookie(key="name", value=user.name) else: resp.status_code = status.HTTP_403_FORBIDDEN @app.get(“/top") def top(resp: Response, name: Optional[str] = Cookie(None)): if name: return {"name": name} resp.status_code = status.HTTP_403_FORBIDDEN return {"message": "please login via /auth"} USERS = [ {"name": “alice”,”password”:”alice1234”}, {"name": “bob”,”password”:”bob5678”}, ] class AuthenticatedUser(HttpUser): def on_start(self): if len(USERS) > 0: user = USERS.pop() logger.info(f"popped user: {user}") self.name = user["name"] self.client.post( "/auth", json={ "name": user["name"], "password": user["password"], }, ) @task def get_name(self): with self.client.get(“/top") as resp: if resp.json()["name"] != self.name: logger.warning("not match") resp.failure() # resp.success() ΋͋Δ 'BTU"1*TBNQMFBQQˣ MPDVTUGJMFˠ
  9. Ϩεϙϯεͷৄࡉͳ֬ೝ7BMJEBUFSFTQPOTFTJOEFUBJM @app.post("/auth") def auth(user: User, resp: Response): req_body = user.dict()

    # {"name": "alice1234", "password": "alice1234"} if user_collection.find_one(req_body): resp.status_code = status.HTTP_200_OK resp.set_cookie(key="name", value=user.name) else: resp.status_code = status.HTTP_403_FORBIDDEN @app.get(“/top") def top(resp: Response, name: Optional[str] = Cookie(None)): if name: return {"name": name} resp.status_code = status.HTTP_403_FORBIDDEN return {"message": "please login via /auth"} USERS = [ {"name": “alice”,”password”:”alice1234”}, {"name": “bob”,”password”:”bob5678”}, ] class AuthenticatedUser(HttpUser): def on_start(self): if len(USERS) > 0: user = USERS.pop() logger.info(f"popped user: {user}") self.name = user["name"] self.client.post( "/auth", json={ "name": user["name"], "password": user["password"], }, ) @task def get_name(self): with self.client.get(“/top") as resp: if resp.json()["name"] != self.name: logger.warning("not match") resp.failure() # resp.success() ΋͋Δ 'BTU"1*TBNQMFBQQˣ MPDVTUGJMFˠ name, passwordΛड͚औͬͯɺ session id୅ΘΓʹnameͷcookieʹηοτ Receive name and password, set name in cookie as if session id
  10. Ϩεϙϯεͷৄࡉͳ֬ೝ7BMJEBUFSFTQPOTFTJOEFUBJM @app.post("/auth") def auth(user: User, resp: Response): req_body = user.dict()

    # {"name": "alice1234", "password": "alice1234"} if user_collection.find_one(req_body): resp.status_code = status.HTTP_200_OK resp.set_cookie(key="name", value=user.name) else: resp.status_code = status.HTTP_403_FORBIDDEN @app.get(“/top") def top(resp: Response, name: Optional[str] = Cookie(None)): if name: return {"name": name} resp.status_code = status.HTTP_403_FORBIDDEN return {"message": "please login via /auth"} USERS = [ {"name": “alice”,”password”:”alice1234”}, {"name": “bob”,”password”:”bob5678”}, ] class AuthenticatedUser(HttpUser): def on_start(self): if len(USERS) > 0: user = USERS.pop() logger.info(f"popped user: {user}") self.name = user["name"] self.client.post( "/auth", json={ "name": user["name"], "password": user["password"], }, ) @task def get_name(self): with self.client.get(“/top") as resp: if resp.json()["name"] != self.name: logger.warning("not match") resp.failure() # resp.success() ΋͋Δ MPDVTUGJMFˠ nameͷ஋Λ֬ೝͯ͠ɺϩάΠϯ֬ೝͯ͠Δ෩ Validate the value of “name” in cookie, pretending to checking if a user is authenticated 'BTU"1*TBNQMFBQQˣ
  11. Ϩεϙϯεͷৄࡉͳ֬ೝ7BMJEBUFSFTQPOTFTJOEFUBJM @app.post("/auth") def auth(user: User, resp: Response): req_body = user.dict()

    # {"name": "alice1234", "password": "alice1234"} if user_collection.find_one(req_body): resp.status_code = status.HTTP_200_OK resp.set_cookie(key="name", value=user.name) else: resp.status_code = status.HTTP_403_FORBIDDEN @app.get(“/top") def top(resp: Response, name: Optional[str] = Cookie(None)): if name: return {"name": name} resp.status_code = status.HTTP_403_FORBIDDEN return {"message": "please login via /auth"} USERS = [ {"name": “alice”,”password”:”alice1234”}, {"name": “bob”,”password”:”bob5678”}, ] class AuthenticatedUser(HttpUser): def on_start(self): if len(USERS) > 0: user = USERS.pop() logger.info(f"popped user: {user}") self.name = user["name"] self.client.post( "/auth", json={ "name": user["name"], "password": user["password"], }, ) @task def get_name(self): with self.client.get(“/top") as resp: if resp.json()["name"] != self.name: logger.warning("not match") resp.failure() # resp.success() ΋͋Δ MPDVTUGJMF UFTUTDFOBSJP ˠ 'BTU"1*TBNQMFBQQˣ ςετ༻Ϣʔβ Users for load testing
  12. Ϩεϙϯεͷৄࡉͳ֬ೝ7BMJEBUFSFTQPOTFTJOEFUBJM @app.post("/auth") def auth(user: User, resp: Response): req_body = user.dict()

    # {"name": "alice1234", "password": "alice1234"} if user_collection.find_one(req_body): resp.status_code = status.HTTP_200_OK resp.set_cookie(key="name", value=user.name) else: resp.status_code = status.HTTP_403_FORBIDDEN @app.get(“/top") def top(resp: Response, name: Optional[str] = Cookie(None)): if name: return {"name": name} resp.status_code = status.HTTP_403_FORBIDDEN return {"message": "please login via /auth"} USERS = [ {"name": “alice”,”password”:”alice1234”}, {"name": “bob”,”password”:”bob5678”}, ] class AuthenticatedUser(HttpUser): def on_start(self): if len(USERS) > 0: user = USERS.pop() logger.info(f"popped user: {user}") self.name = user["name"] self.client.post( "/auth", json={ "name": user["name"], "password": user["password"], }, ) @task def get_name(self): with self.client.get(“/top") as resp: if resp.json()["name"] != self.name: logger.warning("not match") resp.failure() # resp.success() ΋͋Δ MPDVTUGJMFˠ Ϣʔβ͕εϙʔϯ͢Δͱ͖ʹݺ͹ΕΔ Called when a user is spawned ͜ͷηογϣϯ͸͋ͱͷ@taskͷॲཧʹҾ͖ܧ͕ΕΔ This session is shared to following part decorated with @task 'BTU"1*TBNQMFBQQˣ
  13. Ϩεϙϯεͷৄࡉͳ֬ೝ7BMJEBUFSFTQPOTFTJOEFUBJM @app.post("/auth") def auth(user: User, resp: Response): req_body = user.dict()

    # {"name": "alice1234", "password": "alice1234"} if user_collection.find_one(req_body): resp.status_code = status.HTTP_200_OK resp.set_cookie(key="name", value=user.name) else: resp.status_code = status.HTTP_403_FORBIDDEN @app.get(“/top") def top(resp: Response, name: Optional[str] = Cookie(None)): if name: return {"name": name} resp.status_code = status.HTTP_403_FORBIDDEN return {"message": "please login via /auth"} USERS = [ {"name": “alice”,”password”:”alice1234”}, {"name": “bob”,”password”:”bob5678”}, ] class AuthenticatedUser(HttpUser): def on_start(self): if len(USERS) > 0: user = USERS.pop() logger.info(f"popped user: {user}") self.name = user["name"] self.client.post( "/auth", json={ "name": user["name"], "password": user["password"], }, ) @task def get_name(self): with self.client.get(“/top") as resp: if resp.json()["name"] != self.name: logger.warning("not match") resp.failure() else: resp.successe() 'BTU"1*ˣ MPDVTUGJMFˠ Ϣʔβݻ༗ͷ৘ใΛΠϯελϯεม਺ͱͯ͠อ࣋ Keep user-specific value as an instance variable Ϣʔβ͝ͱʹҟͳΔظ଴஋Λ൑ఆ Validate if the expected value is returned ࣦഊ or ੒ޭͱͯ͠ه࿥ Mark as failed or succeeded
  14. ςετσʔλͷॳظԽ1PQVMBUFEBUB w QZUFTUͰ͸GJYUVSFͰ؀ڥ΍σʔλͷ४උɾย෇͚Λ͢Δ 
 :PVVTFGJYUVSFTGPSTFUVQUFBSEPXOJO1ZUFTU w ಉ༷ʹ-PDVTUͰ͸ɺFWFOUIPPLTͰલॲཧɾޙॲཧΛॻ͚Δ 
 6TFFWFOUIPPLTJO-PDVTUGPSUIBU w

    ςετ։࢝࣌ɾऴྃ࣌Ҏ֎ʹ΋৭ʑɺҰཡ͸ެࣜ%PDΛࢀর 
 $IFDLUIFPGGJDJBMEPDGPSUIFDPNQMFUFMJTUPGBWBJMBCMFIPPLT 
 IUUQTEPDTMPDVTUJPFOTUBCMFBQJIUNMFWFOUIPPLT @events.test_start.add_listener def on_test_start(environment, **kwargs): if isinstance(environment.runner, MasterRunner): setup_database() insert_master_data() 
 insert_some_aditional_data() 
 else: # Do worker node setup
  15. ςετσʔλͷॳظԽ1PQVMBUFEBUB w 1ZUIPOͰॻ͘͜ͱͰɺϩʔΧϧ։ൃ༻ͷεΫϦϓτ͔ΒॲཧΛྲྀ༻Ͱ͖Δ 
 3FVTFTFUVQTDSJQUXSJUUFOGPSMPDBMEFWFMPQNFOUJOMPBEUFTUJOH @events.test_start.add_listener def on_test_start(environment, **kwargs): if

    isinstance(environment.runner, MasterRunner): setup_database() insert_master_data() 
 insert_loadtest_user_data() 
 else: # Do worker node setup @click.command(“setup”) def setup_local_env(): setup_database() insert_master_data()
  16. w ςʔϒϧͰྲྀΕΔϩά͸MPHHFSͰ0''ʹ͢Δͱྑ͍͔΋ 
 $POTPMFTUBUTUBCMFDBODMVUUFSZPVSMPH UVSOJUPGG ίϚϯυϥΠϯ͔Βͷىಈ3VOGSPNDPNNBOEMJOF from locust.stats import console_logger

    # You can suppress tables in log by disabling stats logger console_logger.disabled = True Name # reqs # fails | Avg Min Max Median | req/s failures/s ------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------ Aggregated 0 0(0.00%) | 0 0 0 0 | 0.00 0.00 Name # reqs # fails | Avg Min Max Median | req/s failures/s ------------------------------------------------------------------------------------------------------------------------------------------ GET / 2 2(100.00%) | 3 2 4 3 | 0.00 0.00 POST /auth 2 2(100.00%) | 22 8 37 9 | 0.00 0.00 ------------------------------------------------------------------------------------------------------------------------------------------ Aggregated 4 4(100.00%) | 13 2 37 4 | 0.00 0.00
  17. w ͦΕͧΕͷXPSLFS্ͰϢχʔΫͳϢʔβͷ৘ใΛಈతʹੜ੒͢Δ 
 %ZOBNJDBMMZHFOFSBUFVOJRVFVTFSTPOFBDIOPEF  w 66*%Λ࢖ͬͨΓɺϗετ໊Λ࢖ͬͨΓ 
 VTJOH66*%PSIPTUOBNFPGUIFXPSLFSOPEF w

    ౤ೖ͢Δσʔλ͕ෳࡶͳ৔߹ɺFWFOUIPPLͰͷॳظԽॲཧ΋ෳࡶʹ 
 *OJUJBMJ[BUJPODPEFJOFWFOUIPPLDBOCFDPNQMJDBUFEJGJOJUEBUBJTDPNQMJDBUFE Target worker 1 worker 1 Master Alice_worker1 Alice_worker2 Ϋϥελߏ੒Ͱͷࢼݧ࣮ߦ%JTUSJCVUFEXPSLFST
  18. w ͜͜·Ͱ -PDVTU͸ςετγφϦΦΛ1ZUIPOίʔυͱͯ͠ॻ͚Δ 
 -PDVTUDBOEFGJOFUFTUTDFOBSJPBT1ZUIPODPEFT w ͜͜·Ͱ )FBEMFTTʹಈ͔͢͜ͱͰɺύϥϝʔλ΋ίʔυ؅ཧͰ͖ΔΑ͏ʹ 
 1BSBNFUFSTMJLFUIFOVNCFSPGVTFSTBSFOPXVOEFSWFSTJPODPOUSPM

    w ͞ΒʹਐΊͯɺΫϥελߏ੒૊Μ্ͩͰ࣮ߦ؀ڥ΋ίʔυԽͨ͘͠ͳΔ 
 5IFOZPVNBZXBOUUPNBOBHFJUTFYFDVUJPOFOWJSPONFOUBT$PEF w ,VCFSOFUFTͷ+PC͕ͪΐ͏Ͳྑ͔ͬͨ ͜ͷޙσϞ͠·͢ 
 z+PCzSFTPVSDFPG,VCFSOFUFTNBLFTJUFBTZ Ϋϥελߏ੒Ͱͷࢼݧ࣮ߦ%JTUSJCVUFEXPSLFST
  19. apiVersion: batch/v1 kind: Job metadata: name: locust-master labels: app: locust

    role: master spec: completions: 1 template: metadata: labels: app: locust role: master spec: restartPolicy: OnFailure containers: - name: locust image: tatchnicolas/pyconjp2021-locust command: - "locust" - "--master" - "--config" - "/var/locust/locust.conf" apiVersion: batch/v1 kind: Job metadata: name: locust-worker labels: app: locust role: worker spec: completions: 2 parallelism: 2 template: metadata: labels: app: locust role: worker spec: restartPolicy: OnFailure containers: - name: locust image: tatchnicolas/pyconjp2021-locust command: - "locust" - "--worker" env: - name: LOCUST_MASTER_NODE_HOST value: locust-master-svc Master ↓ Worker→ Ϋϥελߏ੒Ͱͷࢼݧ࣮ߦ%JTUSJCVUFEXPSLFST
  20. ·ͱΊ,FZUBLFBXBZT w ΞϓϦέʔγϣϯΛ1ZUIPOͰॻ͍ͨͳΒɺෛՙࢼݧ΋1ZUIPOͰॻ͜͏ 
 *GZPVXSJUFZPVSBQQJO1ZUIPO XIZOPUEPJOHTPGPSMPBEUFTUJOH  w ॻ͖׳ΕͨݴޠͳΒɺ؀ڥ४උ΋ෳࡶͳγφϦΦ΋ࣗ༝ࣗࡏ 


    8SJUFTFUVQTDSJQUBOEUFTUTDFOBSJPJOUIFMBOHVBHFZPVBSFDPOGJEFOUJO w ࠶ݱੑͷ͋ΔܗͰ࣮ߦ͠Α͏ύϥϝʔλ΋؀ڥ΋(JU؅ཧʹ 
 3VOMPBEUFTUJOHXJUISFQSPEVDJCJMJUZ1VUQBSBNFUFSTBOEFOWJSPONFOUTVOEFSWFSTJPODPOUSPM w Ϋϥελߏ੒ͰΑΓڧ͍ෛՙΛ༩͑Α͏ɺ࣮ߦ؀ڥ΋ίʔυԽ͠Α͏ 
 6TFEJTUSJCVUFEXPSLFSTUPQMBDFIJHIFSMPBE BOENBOBHFFYFDVUJPOFOWJSPONFOUXJUI*B$ w εϥΠυͱαϯϓϧίʔυ͸ͪ͜Β 
 4MJEFTBOETBNQMFDPEFTDBOCFGPVOEIFSF w IUUQTTQFBLFSEFDLDPNUBUDIOJDPMBTQZDPOKQMPDVTU w IUUQTHJUIVCDPN5BUDI/JDPMBTQZDPOKQMPDVTU