Shadowing Practice: Distributed Task Scheduler: System Design Interview (Stripe & Amazon Offers) - Learn English Speaking with Video

בחר מצב תרגול

כל מצב פותח חדר תרגול במסך מלא.

324 משפטים
אם המשפטים קצרים או ארוכים מדי, לחץ על ערוך כדי להתאים אותם.
1
in this video we are going to design a distributed task
2
scheduler like aws step functions a distributed task scheduler is a system
3
that is designed to execute arbitrary code
4
or trigger events at a specific time in the future either once
5
or on a recurring basis designing a robust task scheduler requires solving complex distributed systems problems including high precision
6
so a tasks should execute exactly when they are supposed to, maintaining fault tolerance so no tasks are lost if a worker node crashes,
7
and avoiding duplicate executions in a highly concurrent environment.
8
And so this is a really popular system design that is asked in interviews by Meta, Amazon, and Microsoft.
9
And they love to ask it because it exposes your understanding of distributed system trade -offs, which is a great way of differentiating the good candidates from the great ones.
10
For those of you
11
that are struggling with system design I highly recommend you check
12
out Tech Prep where you can see the full write -up
13
and the interactive AI whiteboard
14
that walks you through the design exactly like how an interviewer would
15
and it helped me go from projections to offers from the likes of Stripe and Amazon.
16
There are also a lot of system design concepts and patterns
17
that may be new to you in this system design and
18
so one way to keep up is to use the AI
19
highlighter where you can highlight anything in the write -up
20
and it will give you a full explanation based on the context of this system design.
21
So without further ado let's jump into the requirements.
22
So for the functional requirements firstly we will need task submission
23
so users can submit one -off delayed tasks or recurring tasks e .g cron expressions.
24
For task execution the system must execute the provided task payload so this could be a HTTP webhook, a script or a message push at the scheduled time.
25
In terms of task management users can query the state of a task so it can be pending,
26
running, completed, failed or cancelled and users should be able to cancel an upcoming task.
27
We should also then have an execution history
28
so the system must store the output and execution logs of completed tasks for auditing purposes.
29
Then for the non -functional requirements firstly scalability the system must handle 10 000 plus task submissions per second
30
and peak execution bursts of 50 000 tasks per second in
31
terms of high availability we'll want 99 .99 uptime for task submission
32
and execution routing and then for reliability we want no data loss
33
so once the task is acknowledged as accepted it must be durably stored
34
and guaranteed to execute and then finally in terms of precision
35
and latency tasks should begin execution within 200 milliseconds of their scheduled time.
36
So now that we've looked at the requirements let's look at the data model we need to build to fulfill those requirements.
37
So to handle the contrasting demand of strict state management
38
and the massive write -heavy logging I think a polyglot persistence strategy is required for a system of this scale
39
and so what polyglot persistence simply means is
40
that we have to use different data storage types for different things and I'm going to walk through each of them now.
41
So the first thing will be our relational database which you can implement with Postgres.
42
So this will serve as the system's source of truth for task definitions, schedules and metadata and we rely heavily on its asset properties
43
and row level locking to manage the state machine of a task without race conditions.
44
And the core entities will include a task definition
45
so this is the core configuration object it stores the user's
46
intent the payload to be executed for example http webhook url
47
and body the retry policy and the scheduling logic it's either one off
48
or a cron expression next will be our task execution
49
so this represents a single distinct run of a task definition
50
and it acts as the primary state machine
51
so we'll go from pending to queued to running to either success
52
or failed and it includes an item potency key and tracks exactly
53
when the task was picked up by a worker and when it finished.
54
So this is really important and is the kind of core of our architecture.
55
Next we'll also need an in -memory store which we can implement with Redis and this is also super important.
56
So this will act as the high speed timer and scheduling buffer
57
and we will also utilize Redis sorted sets
58
so z sets where the score is in the epoch execution time
59
and this will allow the system to query for tasks
60
that need to run right now in o log n time bypassing the need to constantly scan the relational database.
61
Now if you don't know what red assorted sets are don't worry about
62
that we're going to explain those in detail later on.
63
We'll also need a message broker so something implemented with either Kafka
64
or RabbitMQ which will serve as the durable execution queue and
65
so it decouples the scheduling components from the worker pool allowing the system to absorb massive spikes in task executions
66
so back pressure without overwhelming the database
67
and finally we'll also need a wide column store
68
so something like Cassandra and this is used exclusively for execution logs
69
and history since a single recurring task might generate millions of
70
logs over its lifetime a high write throughput horizontally scalable no sql database is ideal for this
71
and so that's the data model so
72
if you understand these four data storage techniques
73
and why they're used you will be in a really good place to move on to our api design
74
so for a distributed task scheduler we can just use a classic restful API to interact with the data.
75
RESTful API is simple, widely used, stateless and they support caching which makes it a great candidate for our system.
76
So the first endpoint can be a create task endpoint.
77
So this will be a post to a v1 tasks.
78
It accepts a payload, execution time or a cron and a target and will return a task ID.
79
Next will be the get task status.
80
This will be a get request to the v1 tasks task id
81
and it will return the current state and metadata of that task.
82
A cancel task endpoint so this will be a delete at v1 tasks task id
83
and it marks a pending task as cancelled
84
and then finally for logs we can get a get logs endpoint
85
which will be a get to a v1 tasks task id executions
86
and this will return a paginated execution history of that task.
87
Obviously production system will have a lot more endpoints
88
but these are the kind of core endpoints to outline to your interviewer what's kind of core to this system
89
and what we'll need to cover as we go through our high -level system design.
90
So now that we've mentioned our data model and API design let's look at our high -level design.
91
So at a high level the system architecture can be separated in terms of accepting a task as well as executing it.
92
So the first part will be the submission path.
93
So this path focuses on securely receiving the task and durably persisting its schedule.
94
So here a client will upload it'll go to the gateway
95
this will route the incoming request it'll also authenticate the client
96
and enforce rate limits this will then get pushed to our task management service
97
which validates the payload and cron expressions
98
and it writes the task definition to the task database again Postgres with this state of pending and then
99
if the task is scheduled to run in the very near
100
future e .g within the next 15 minutes the service could
101
also push a lightweight reference to the task directly into the redis zset timer next is the execution path
102
and so this path focuses on time triggering the task
103
and hand and handing it off to worker nodes
104
so firstly we'll have relay nodes this will be a background batch process
105
that periodically queries the task database postgres for tasks scheduled to run in the near future for example 10 minutes
106
and stages them in the redis zset timer this prevents the database from being continuously hammered by high frequency polling
107
and then what we can have is we can have a fleet of lightweight scheduler nodes
108
which will continuously run a process let's say every 100 milliseconds
109
to pull the redis z set for tasks whose scheduled time is less than
110
or equal to the current time once we've pulled those tasks
111
off the redis z set we can then add them to a task queue
112
and so the task payload is fetched
113
and pushed onto this highly available message queue
114
and to prevent noisy neighbors where one customer submitting 50 000 tasks saturates the queue
115
and delays others tasks are rooted to tenant specific queues using a hash of the tenant id
116
and this allows workers to round robin between tenant queues ensuring
117
equitable compute distribution among our customers next we'll have our worker nodes
118
and so this is a fleet of horizontally scaled compute nodes
119
that consume messages from the queue execute the business logic eg
120
make the request to the target system the http webhook
121
and then report the successor failure to the task database
122
and then keep a historical log in cassandra
123
so pretty simple system it's not too crazy and so
124
while this high level design does separate our concerns it does contain critical flaws
125
so pulling our database efficiently we can dive deeper into
126
that workers can crash mid -execution leaving tasks stuck in limbo
127
which we'll have to deal with
128
and network retries could cause a payment task to execute twice
129
and so we have to deep diaper into all of these and show how we can resolve them.
130
So firstly let's look at Redis Z sets and how they're implemented so we have a better understanding.
131
So Redis sorted sets is an in -memory data structure that automatically orders unique elements by a specific score.
132
In this case the Unix execution timestamp allowing the scheduler to instantly identify
133
and extract tasks run in o log n time without constantly polling
134
and overwhelming our primary database and
135
so under the hood the way it's implemented is two data structures the first one is a hash map
136
and this is used for o1 point lookups
137
so the purpose of this is to get
138
that instant access via task id and
139
so the situation this would be used is like
140
if a user wants to cancel an upcoming task
141
or the system needs to check its exact scheduled time can check via the task
142
id to get its execution timestamp
143
and redis ignores time -based sorting entirely by consulting this internal hash map to find by key
144
and instantly return the execution timestamp in o of one time
145
so it doesn't have to traverse any list the second data structure is the skip list
146
and this is for o log n time -based polling and
147
so here this is the purpose of this is for fast traversal by execution timestamp.
148
And so the scenario we use this is like the schedule node polls for tasks ready to execute right now.
149
So we'll check for tasks that's execution time is less than or equal to right now.
150
And so obviously Redis cannot use the hash map here because it doesn't know which specific task IDs are right for execution.
151
Instead it goes to this skip list. And
152
so it uses the upper level express lanes to quickly skip
153
over millions of tasks scheduled far into the future like task C
154
and it drops down to the base level only
155
when it nears the target timestamp landing directly on the threshold of ready tasks to instantly extract them for the message queue.
156
So if we look at a quick example let's say we're polling for ready tasks
157
and the current timestamp is this one here on the screen.
158
Okay well the first thing we're going to do is start at level three start at our head
159
and then look right and when we look right we can see we have task c
160
which as you can see is greater than our current time stamp as we can see that's in the future
161
so we can ignore it so what do we do
162
so we drop down to the next level
163
and again we start out our head and we look to the right
164
and as you can see we move across
165
and we can see the time stamp is exactly equal to our current time stamp
166
and so therefore we know we have found our upper bound
167
and so what we now need to do is find all the elements up to
168
that upper bound and the way we do
169
that is drop down again a level and
170
because we're at the bottom level here we know this is a perfectly sorted link list
171
and so redis instantly knows
172
that any node from the head to task b is right for execution
173
so it sequentially traverses the level one extracting task a
174
and task b and returning them to the scheduler to be pushed to the kafka queue
175
so hopefully that makes sense
176
and again the primary architectural benefit here is we are able
177
to extract ready to run tasks in o log n time without constantly polling and overwhelming the primary database.
178
So next we need a look at high precision scheduling.
179
So running a select all from the tasks table in our
180
Postgres database containing billions of rows every second will instantly melt the database.
181
We need a way to track time efficiently to achieve that less than 200 milliseconds execution precision without database bottlenecking.
182
And so we will implement a push -pull time partitioned architecture.
183
So the first step, again, we have our luck ahead cron.
184
So this background relay worker will wake up every five minutes
185
and it'll query the Postgres database for all tasks scheduled to run in the next 10 minutes.
186
Then we'll have our Redis sorted set.
187
The relay will push these upcoming task IDs into the Redis Z set.
188
And the score in the Z set again is that exact Unix epoch timestamp of when the task should run.
189
Then we will have sub second polling.
190
So a fleet of lightweight scheduler nodes will pull the Redis Z set every 100 milliseconds.
191
looking for tasks where the score is less than or equal to the current timestamp.
192
Then we will have an atomic handoff, so using a Lua script.
193
So to prevent multiple scheduler nodes from pulling the exact same task during concurrent polling, we use a custom Lua script.
194
And because Redis executes Lua scripts as a single indivisible transaction on its single threaded event loop, it guarantees an atomic check and pop.
195
And so this means that the script queries for the ready tasks and immediately deletes them,
196
safely handing the tasks to exactly one scheduler to then push onto the message queue.
197
And then finally, to mitigate thundering herds.
198
So, for example, humans will naturally schedule cron jobs at clean intervals, for example, every midnight.
199
And so to prevent massive artificial latency spikes at the top of the hour, the schedule can inject optional execution jitter,
200
which will add a random number of seconds between, let's say, 0 and 300 delay for tasks
201
that do not require strict millisecond precision so that we can smooth out that load curve,
202
ensuring that we don't have massive spikes in executions.
203
And so the architectural benefit here is the heavy relational database is only queried sequentially in predictable batches,
204
while the high frequency sub -second polling and lock frequency concurrency control is handled entirely in memory by Redis.
205
Next, we need to look at our worker fault tolerance.
206
So a major distributed system problem is the stuck task.
207
So what happens if a worker node pulls a critical task from the queue, but the server suffers a hard crash before finishing?
208
Well, the task must not be lost, nor can it be duplicated prematurely.
209
So to guarantee at least once delivery and fault tolerance,
210
we rely on visibility timeouts and explicit acknowledgements natively supported by brokers like AWS SQS or RabbitMQ.
211
what we need is
212
when a worker pulls a message the broker does not delete
213
it instead it will update the metadata attribute on the message
214
for example we could have a field invisible underscore until timestamp and so
215
when other workers pull the queue the broker's internal query simply
216
filters out messages where the invisible until is greater than the current time
217
because that is currently being executed
218
and we should not touch it we'll also have the receipt handle
219
and so upon pulling the message the broker also gives the worker a unique temporary identifier
220
so this receipt handle or delivery tag and we'll see why that's important in just a second.
221
We'll also have heartbeats so this is the visibility extension so
222
if a task naturally takes a long time a background thread on the worker periodically pings the broker to overwrite
223
and extend the invisible until timestamp actively maintaining its exclusive lock and preventing other workers from touching it.
224
We'll also have the ACK which is the final deletion and
225
so upon successful completion the worker sends the receipt handle
226
which we saw previously back to the broker in an ACK request
227
and the broker uses this handle to locate the message in O of one time
228
and permanently hard delete it from its memory or disk so that it is never picked up again by any other worker.
229
And then in the case of handling the crash well
230
if a worker crashes it stops sending the heartbeats
231
and never sends the ACK and so once the invisible until timestamp expires
232
and it's in the past the message naturally becomes visible again for a healthy worker node to claim
233
and process and so this is a really nice way of handling
234
that worker fault tolerance that most candidates won't be able to answer.
235
Next we need to look at execution guarantees and
236
because our fault tolerance mechanism relies on timeouts
237
and retries we guarantee at least once execution however this means
238
that a task might run twice and
239
so strict exactly once delivery is theoretically impossible over an unreliable
240
network therefore we must implement idempotency to make duplicate executions harmless
241
so we need optimistic concurrency control
242
so before a worker executes a payload it attempts to update the task status in postgres
243
so you'll update the status to running
244
and increment its version number by one right and so this means
245
if the worker crashed previously
246
and a duplicate worker picks up the same task the database will reject the second update
247
because the status is no longer pending
248
and the worker knows to safely discard the duplicate and again
249
that version number acts as a unique constraint
250
so once one worker increments it from one to two the second worker's update fails
251
because it will still be looking for version one and
252
so this implements the optimistic locking ensuring exactly one worker can claim a task even under high concurrency
253
and the version number means the check and update operation truly is atomic
254
and we'll also need client side item potency keys
255
so for HTTP webhook tasks the scheduler will inject an item
256
potency key header generated from the task id plus the execution timestamp into the outbound request
257
and the downstream system receiving the webhook is responsible for using this key to ignore duplicate HTTP calls
258
and so by using these mechanisms we are improving our execution guarantees.
259
And then finally looking at scaling our storage layer
260
because handling 10 ,000 plus continuous task commissions per second pushes the limits of single node architectures.
261
This translates to 10 ,000 plus database inserts operations per second plus B3 index updates all
262
while serving continuous select queries from the relay components.
263
And so to prevent severe CPU and disk IO bottlenecks both the relational database
264
and the in -memory Redis cluster must be horizontally partitioned
265
so firstly looking at postgres you know
266
while a heavily provisioned single postgres primary can technically peak at 10 000 tps running it at
267
that threshold and production guarantees latency spikes and lock contention and
268
so firstly we'll need horizontal sharding
269
so we distribute the right load across multiple independent postgresql clusters
270
for a multi -tenant sas platform we shard based on tenant id
271
and this ensures data locality
272
so all tasks for specific customer reside on the same database node keeping transactions highly localized
273
and by provisioning 10 distinct nodes the load drops to a highly manageable 1000 tps per primary
274
and then we'll also have time -based table partitioning
275
so as the system scales to billions of executed tasks the
276
active database shard will become bloated slowing down the look ahead cron queries
277
and so we can mitigate this by partitioning the task tables by month
278
or week and
279
so this allows the system to quickly drop historical partitions archiving
280
them to cold storage like s3 without running expensive delete queries and keeping the active b3 index incredibly small and fast
281
next looking at redis and so
282
because redis operates entirely in memory a single instance can handle
283
100 000 simple operations per second however a single node creates a single point of failure
284
and a massive single sorted set degrades performance and
285
so we can see time complexity duration so because we rely on zadd
286
and z range by score queries both of
287
which operate o log n time as a single z set
288
grows to millions of elements the single threaded redis event loop will begin to block
289
and so therefore we will need time bucketed keys
290
so instead of one global schedule task zset we can partition
291
zsets by time bucket for example tasks minute you know 2 30
292
and tasks minute 2 31 and as well as
293
that we can also have a hash slot distribution
294
so by deploying a redis cluster these time bucketed keys are deterministically hashed
295
and distributed across redis primary nodes and this horizontally scales both the memory footprint
296
and the olog n compute overhead ensuring this layer never blocks under massive concurrency
297
and so look we've gone deep on you know high precision scheduling worker fault tolerance execution guarantees
298
and scaling the storage layer so
299
if you need know these things these are the core things interviewers are going to ask you
300
so knowing them in depth will really bulletproof you
301
when you go into your interview and
302
so to finish up let's just walk through the complete architecture one last time
303
so you've kind of got that one mental snapshot of understanding how everything works together
304
so a client will submit a payload to the API gateway
305
that will then get routed to the task management service which writes
306
that persistent configuration to Postgres the relay service every few minutes
307
in a batch process will pull tasks scheduled for the near future from Postgres
308
and stage them to our Redis Z set keyed by their
309
precise execution epoch time high frequency scheduler nodes then continuously pull Redis
310
and when a task's time arrives a Lua script atomically pops it from redis
311
and pushes it onto a partitioned kafka
312
or rabbit mqq utilizing tenant -based routing to avoid noisy neighbors
313
a worker node claims a task triggering a visibility timeout the
314
worker then updates the database state to running using optimistic locking
315
to prevent concurrent execution races the worker then executes the arbitrary code
316
or http webhook injecting an item potency key to protect downstream systems from network retries
317
and the worker then sends an act to the queue to permanently delete the message updates the state
318
and postgres to complete it
319
and flushes the execution logs asynchronously into the cassandra wide column
320
store for permanent auditing to check out the full write -up
321
and the ai whiteboard head over to techprep .app
322
and hopefully you got some value out of this
323
if you did please like and subscribe
324
and share with a friend it helps the channel out a lot and i will see you in the next one

הורד אפליקציה

כל מה שאתה צריך כדי לדבר בשטף

הגיית AIציון לכל משפט
תרגול IPAשלוט בכל צליל
אוצר מיליםבנה את מאגר המילים שלך
משחק אוצר מיליםלמד תוך כדי משחק

אותו ערוץ

FOR YOU

המלץ על סרטון

אוצר מילים והערות הגייה לשיעור הזה

שיעור הדיבור הזה ברמה C1 מבוסס על הסרטון “Distributed Task Scheduler: System Design Interview”. המילים שחוזרות הכי הרבה: task, execution, worker, redis, database. בסרטון הזה יש 324 משפטים ו-4079 מילים לתרגול Shadowing. הדיבור נמשך 22:56. הדובר מדבר בקצב טבעי של כ-178 מילים בדקה, קרוב לשיחה יומיומית. רק 73% מהמילים נמצאות בין 3,000 המילים הנפוצות ביותר באנגלית, כך שאוצר המילים מאתגר.

אוצר מילים מרכזי בסרטון הזה

15 המילים המתקדמות ביותר בסרטון, עם ההגייה והמשמעות שלהן:

מילההגייהמשמעות
queue שם עצם/kju/תּוֹר
execute פועל/ˈɛksɪˌkjuːt/להוציא להרוג
distribute פועל/dɪˈstɹɪbjuːt/חילק
broker שם עצם/ˈbɹoʊkɚ/בְּרוֹקֶר
tolerance שם עצם/ˈtɑ.lə.ɹəns/סובלנות
hash שם עצם/ˈhæʃ/סוּלָּמִית
potency שם עצם/ˈpəʊtənsi/כוח
invisible שם תואר/ɪnˈvɪzəb(ə)l/בלתי־נראה
concurrent שם תואר/kəŋˈkʌɹənt/מַקְבִּיל
epoch שם עצם/ˈɛp.ək/תְּקוּפָה
timer שם עצם/ˈtaɪmɚ/טיימר
optimistic שם תואר/ˌɒptɪˈmɪstɪk/אוֹפְּטִימִי
atomic שם תואר/əˈtɑː.mɪk/אָטוֹמִי
extract שם עצם/ˈɛkstɹækt/תַּמְצִית
shard שם עצם/ˈʃɑːd/רְסִיס

Phrasal verbs שתשמעו

מילההגייהמשמעות
pick up פועלהרים, אסף
check out פועלבדק
wake up פועל/ˌweɪk ˈʌp/התעורר

דקדוק בסרטון הזה

המבנים שהדובר משתמש בהם הכי הרבה, עם המילים המדויקות מהסרטון:

מבנהבסרטון
סביל (Passive voice) be + past participle — הדגש הוא על מה שקורה, לא על מי שעושה את הפעולהis designed · are lost · is asked
הווה מושלם (Present perfect) have/has + past participle — פעולה שקרתה בעבר ועדיין יש לה חשיבות או השפעה עכשיוwe've looked · we've mentioned · we've pulled
משפטי זיקה (Relative clauses) who / which + clause — מידע נוסף על אדם או דברoffs, which is · RabbitMQ which will · caching which makes

הגייה ששווה לשים לב אליה

הדובר משתמש ב-40 קיצורים וצורות מקוצרות, כמו we'll, we're, we've. אמרו אותם בצורה הקצרה, כפי שאתם שומעים אותם.

  • הצלילים של “sh” ו-“zh”: scheduler /ˈʃɛdjələ/, precision /pɹɪˈsɪʒ.ən/, hash /ˈhæʃ/, partition /pɑɹˈtɪ.ʃən/, submission /səbˈmɪʃən/
  • מילים ארוכות — שימו לב להטעמה: concurrency /kəŋˈkʌɹənsi/, millisecond /ˈmɪlɪˌsɛkənd/, invisible /ɪnˈvɪzəb(ə)l/, metadata /ˈmɛt.əˌdeɪ.tə/, visibility /ˌvɪz.əˈbɪl.ɪ.ti/

צלילים שדוברי עברית מתקשים בהם:

  • /w/ — שפתיים מעוגלות, לא /v/: query /ˈkwɪɹ.i/, webhook /ˈwɛb.hʊk/, overwhelm /ˌoʊ.vɚˈwɛlm/, lightweight /ˈlaɪt.weɪt/, sequentially /səˈkwɛntʃəli/
  • /æ/ — פתוח יותר מה-“אֶ” העברית, לא כמו “a” ב-“cup”: timestamp /ˈtaɪmˌstæmp/, hash /ˈhæʃ/, extract /ˈɛkstɹækt/, batch /bæt͡ʃ/
  • /r/ — הלשון לא נוגעת בשום מקום, לא ה-“ר” הגרונית: query /ˈkwɪɹ.i/, distribute /dɪˈstɹɪbjuːt/, firstly /ˈfɜɹstli/, broker /ˈbɹoʊkɚ/, precision /pɹɪˈsɪʒ.ən/

איך לתרגל עם הסרטון הזה

  1. הקשיבו לכל הסרטון פעם אחת בלי לדבר, ורשמו את המילים שאתם לא מכירים.
  2. התחילו במהירות 0.75×, תרגלו Shadowing משפט אחר משפט, וחזרו למהירות רגילה כשזה מרגיש קל.
  3. הקליטו את עצמכם והשוו למקור, תוך תשומת לב למילים כמו queue, execute, distribute.

מהי טכניקת השאדואינג?

שאדואינג היא טכניקת למידת שפה מבוססת מדע שפותחה במקור להכשרת מתורגמנים מקצועיים ופופולרית על ידי הפוליגלוט ד"ר אלכסנדר ארגואלס. השיטה פשוטה אך עוצמתית: אתה מקשיב לאודיו באנגלית ילידית וחוזר עליו בקול רם מיד — כמו צל העוקב אחרי הדובר עם עיכוב של 1–2 שניות בלבד. בניגוד להאזנה פסיבית או תרגילי דקדוק, שאדואינג מכריח את המוח והשרירים בפה לעבד ולשחזר דפוסי דיבור אמיתיים בו זמנית. מחקרים מראים שזה משפר משמעותית את דיוק ההגייה, האינטונציה, הקצב, הדיבור המחובר, הבנת השמיעה ושטף הדיבור — מה שהופך אותו לאחת השיטות היעילות ביותר להכנה לIELTS Speaking ולתקשורת באנגלית בעולם האמיתי.

טכניקת Shadowing: קרא את המדריך המלא שלב אחר שלב →